From 16e203023bd7b7023cb6a539e2f85b3f1da53df0 Mon Sep 17 00:00:00 2001 From: Daniel Zullo Date: Fri, 19 Jan 2024 17:18:13 +0100 Subject: [PATCH] Add tests for 3-phase active power streaming Signed-off-by: Daniel Zullo --- tests/microgrid/test_component_data.py | 3 + tests/timeseries/mock_resampler.py | 24 ++++- tests/timeseries/test_power_streamer.py | 117 ++++++++++++++++++++++++ tests/utils/component_data_wrapper.py | 18 ++++ 4 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 tests/timeseries/test_power_streamer.py diff --git a/tests/microgrid/test_component_data.py b/tests/microgrid/test_component_data.py index 02e411458..b9632cd21 100644 --- a/tests/microgrid/test_component_data.py +++ b/tests/microgrid/test_component_data.py @@ -53,14 +53,17 @@ def test_inverter_data() -> None: phase_1=electrical_pb2.AC.ACPhase( current=metrics_pb2.Metric(value=12.3), voltage=metrics_pb2.Metric(value=229.8), + power_active=metrics_pb2.Metric(value=3109.8), ), phase_2=electrical_pb2.AC.ACPhase( current=metrics_pb2.Metric(value=23.4), voltage=metrics_pb2.Metric(value=230.0), + power_active=metrics_pb2.Metric(value=3528.3), ), phase_3=electrical_pb2.AC.ACPhase( current=metrics_pb2.Metric(value=34.5), voltage=metrics_pb2.Metric(value=230.2), + power_active=metrics_pb2.Metric(value=3680.1), ), ), ), diff --git a/tests/timeseries/mock_resampler.py b/tests/timeseries/mock_resampler.py index 8489c9179..0d6657874 100644 --- a/tests/timeseries/mock_resampler.py +++ b/tests/timeseries/mock_resampler.py @@ -21,7 +21,7 @@ NON_EXISTING_COMPONENT_ID, ) -# pylint: disable=too-many-instance-attributes +# pylint: disable=too-many-instance-attributes disable=too-many-locals class MockResampler: @@ -159,6 +159,18 @@ def voltage_senders(ids: list[int]) -> list[list[Sender[Sample[Quantity]]]]: ), ) + def active_power_senders( + ids: list[int], + ) -> list[list[Sender[Sample[Quantity]]]]: + return multi_phase_senders( + ids, + ( + ComponentMetricId.ACTIVE_POWER_PHASE_1, + ComponentMetricId.ACTIVE_POWER_PHASE_2, + ComponentMetricId.ACTIVE_POWER_PHASE_3, + ), + ) + self._bat_inverter_current_senders = current_senders(bat_inverter_ids) self._pv_inverter_current_senders = current_senders(pv_inverter_ids) self._ev_current_senders = current_senders(evc_ids) @@ -166,6 +178,7 @@ def voltage_senders(ids: list[int]) -> list[list[Sender[Sample[Quantity]]]]: self._meter_current_senders = current_senders(meter_ids) self._meter_voltage_senders = voltage_senders(meter_ids) + self._meter_active_power_senders = active_power_senders(meter_ids) self._next_ts = datetime.now() @@ -325,3 +338,12 @@ async def send_meter_voltage(self, values: list[list[float | None]]) -> None: for phase, value in enumerate(meter_values): sample = self.make_sample(value) await chan[phase].send(sample) + + async def send_meter_active_power(self, values: list[list[float | None]]) -> None: + """Send the given values as resampler output for meter active power.""" + assert len(values) == len(self._meter_active_power_senders) + for chan, meter_values in zip(self._meter_active_power_senders, values): + assert len(meter_values) == 3 # 3 values for phases + for phase, value in enumerate(meter_values): + sample = self.make_sample(value) + await chan[phase].send(sample) diff --git a/tests/timeseries/test_power_streamer.py b/tests/timeseries/test_power_streamer.py new file mode 100644 index 000000000..94f0419f4 --- /dev/null +++ b/tests/timeseries/test_power_streamer.py @@ -0,0 +1,117 @@ +# License: MIT +# Copyright © 2024 Frequenz Energy-as-a-Service GmbH + +"""Tests for fetching and streaming the 3-phase active power.""" + + +import asyncio + +from pytest_mock import MockerFixture + +from frequenz.sdk import microgrid + +from .mock_microgrid import MockMicrogrid + +# pylint: disable=protected-access + + +async def test_active_power_1(mocker: MockerFixture) -> None: + """Test the 3-phase active power with a grid side meter.""" + mockgrid = MockMicrogrid(grid_meter=True, mocker=mocker) + mockgrid.add_batteries(1, no_meter=True) + mockgrid.add_batteries(1, no_meter=False) + + async with mockgrid: + active_power = microgrid._active_power() + active_power_recv = active_power.new_receiver() + + assert active_power._task is not None + # Wait for active power requests to be sent, one request per phase. + for _ in range(3): + await asyncio.sleep(0) + + for count in range(10): + watts_delta = 1 if count % 2 == 0 else -1 + watts_phases: list[float | None] = [ + 220.0 * watts_delta, + 219.8 * watts_delta, + 220.2 * watts_delta, + ] + + await mockgrid.mock_resampler.send_meter_active_power( + [watts_phases, watts_phases] + ) + + val = await active_power_recv.receive() + assert val is not None + assert val.value_p1 and val.value_p2 and val.value_p3 + assert val.value_p1.as_watts() == watts_phases[0] + assert val.value_p2.as_watts() == watts_phases[1] + assert val.value_p3.as_watts() == watts_phases[2] + + +async def test_active_power_2(mocker: MockerFixture) -> None: + """Test the 3-phase active power without a grid side meter.""" + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) + mockgrid.add_batteries(1, no_meter=False) + mockgrid.add_batteries(1, no_meter=True) + + async with mockgrid: + active_power = microgrid._active_power() + active_power_recv = active_power.new_receiver() + + assert active_power._task is not None + # Wait for active power requests to be sent, one request per phase. + for _ in range(3): + await asyncio.sleep(0) + + for count in range(10): + watts_delta = 1 if count % 2 == 0 else -1 + watts_phases: list[float | None] = [ + 220.0 * watts_delta, + 219.8 * watts_delta, + 220.2 * watts_delta, + ] + + await mockgrid.mock_resampler.send_meter_active_power([watts_phases]) + + val = await active_power_recv.receive() + assert val is not None + assert val.value_p1 and val.value_p2 and val.value_p3 + assert val.value_p1.as_watts() == watts_phases[0] + assert val.value_p2.as_watts() == watts_phases[1] + assert val.value_p3.as_watts() == watts_phases[2] + + +async def test_active_power_3(mocker: MockerFixture) -> None: + """Test the 3-phase active power with None values.""" + mockgrid = MockMicrogrid(grid_meter=True, mocker=mocker) + mockgrid.add_batteries(2, no_meter=False) + + async with mockgrid: + active_power = microgrid._active_power() + active_power_recv = active_power.new_receiver() + + assert active_power._task is not None + # Wait for active power requests to be sent, one request per phase. + for _ in range(3): + await asyncio.sleep(0) + + for count in range(10): + watts_delta = 1 if count % 2 == 0 else -1 + watts_phases: list[float | None] = [ + 220.0 * watts_delta, + 219.8 * watts_delta, + 220.2 * watts_delta, + ] + + await mockgrid.mock_resampler.send_meter_active_power( + [watts_phases, [None, None, None], [None, 219.8, 220.2]] + ) + + val = await active_power_recv.receive() + assert val is not None + assert val.value_p1 and val.value_p2 and val.value_p3 + assert val.value_p1.as_watts() == watts_phases[0] + assert val.value_p2.as_watts() == watts_phases[1] + assert val.value_p3.as_watts() == watts_phases[2] diff --git a/tests/utils/component_data_wrapper.py b/tests/utils/component_data_wrapper.py index 261757ada..51ce9fa31 100644 --- a/tests/utils/component_data_wrapper.py +++ b/tests/utils/component_data_wrapper.py @@ -103,6 +103,11 @@ def __init__( # pylint: disable=too-many-arguments active_power: float = math.nan, current_per_phase: tuple[float, float, float] = (math.nan, math.nan, math.nan), voltage_per_phase: tuple[float, float, float] = (math.nan, math.nan, math.nan), + active_power_per_phase: tuple[float, float, float] = ( + math.nan, + math.nan, + math.nan, + ), active_power_inclusion_lower_bound: float = math.nan, active_power_exclusion_lower_bound: float = math.nan, active_power_inclusion_upper_bound: float = math.nan, @@ -124,6 +129,7 @@ def __init__( # pylint: disable=too-many-arguments active_power=active_power, current_per_phase=current_per_phase, voltage_per_phase=voltage_per_phase, + active_power_per_phase=active_power_per_phase, active_power_inclusion_lower_bound=active_power_inclusion_lower_bound, active_power_exclusion_lower_bound=active_power_exclusion_lower_bound, active_power_inclusion_upper_bound=active_power_inclusion_upper_bound, @@ -163,6 +169,11 @@ def __init__( # pylint: disable=too-many-arguments active_power_exclusion_lower_bound: float = math.nan, active_power_inclusion_upper_bound: float = math.nan, active_power_exclusion_upper_bound: float = math.nan, + active_power_per_phase: tuple[float, float, float] = ( + math.nan, + math.nan, + math.nan, + ), frequency: float = 50.0, cable_state: EVChargerCableState = EVChargerCableState.UNSPECIFIED, component_state: EVChargerComponentState = EVChargerComponentState.UNSPECIFIED, @@ -182,6 +193,7 @@ def __init__( # pylint: disable=too-many-arguments active_power_exclusion_lower_bound=active_power_exclusion_lower_bound, active_power_inclusion_upper_bound=active_power_inclusion_upper_bound, active_power_exclusion_upper_bound=active_power_exclusion_upper_bound, + active_power_per_phase=active_power_per_phase, frequency=frequency, cable_state=cable_state, component_state=component_state, @@ -213,6 +225,11 @@ def __init__( # pylint: disable=too-many-arguments active_power: float = math.nan, current_per_phase: tuple[float, float, float] = (math.nan, math.nan, math.nan), voltage_per_phase: tuple[float, float, float] = (math.nan, math.nan, math.nan), + active_power_per_phase: tuple[float, float, float] = ( + math.nan, + math.nan, + math.nan, + ), frequency: float = math.nan, ) -> None: """Initialize the MeterDataWrapper. @@ -226,6 +243,7 @@ def __init__( # pylint: disable=too-many-arguments active_power=active_power, current_per_phase=current_per_phase, voltage_per_phase=voltage_per_phase, + active_power_per_phase=active_power_per_phase, frequency=frequency, )