diff --git a/pyproject.toml b/pyproject.toml index f959eaed6..3c1abca8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -155,8 +155,10 @@ disable = [ "unsubscriptable-object", # Checked by flake8 "line-too-long", - "unused-variable", + "redefined-outer-name", "unnecessary-lambda-assignment", + "unused-import", + "unused-variable", ] [tool.pylint.design] diff --git a/tests/actor/power_distributing/test_power_distributing.py b/tests/actor/power_distributing/test_power_distributing.py index 85881fc33..6de6e50b6 100644 --- a/tests/actor/power_distributing/test_power_distributing.py +++ b/tests/actor/power_distributing/test_power_distributing.py @@ -10,8 +10,9 @@ import math import re from collections import abc +from contextlib import asynccontextmanager from datetime import timedelta -from typing import TypeVar +from typing import AsyncIterator, TypeVar from unittest.mock import MagicMock from frequenz.channels import Broadcast, Sender @@ -60,7 +61,7 @@ @dataclasses.dataclass(frozen=True) -class Mocks: +class _Mocks: """Mocks for the tests.""" microgrid: MockMicrogrid @@ -78,12 +79,12 @@ async def new( mocker: MockerFixture, graph: _MicrogridComponentGraph | None = None, grid_meter: bool | None = None, - ) -> Mocks: + ) -> _Mocks: """Initialize the mocks.""" - mockgrid = MockMicrogrid(graph=graph, grid_meter=grid_meter) + mockgrid = MockMicrogrid(graph=graph, grid_meter=grid_meter, mocker=mocker) if not graph: mockgrid.add_batteries(3) - await mockgrid.start(mocker) + await mockgrid.start() # pylint: disable=protected-access if microgrid._data_pipeline._DATA_PIPELINE is not None: @@ -114,6 +115,21 @@ async def stop(self) -> None: # pylint: enable=protected-access +@asynccontextmanager +async def _mocks( + mocker: MockerFixture, + *, + graph: _MicrogridComponentGraph | None = None, + grid_meter: bool | None = None, +) -> AsyncIterator[_Mocks]: + """Initialize the mocks.""" + mocks = await _Mocks.new(mocker, graph=graph, grid_meter=grid_meter) + try: + yield mocks + finally: + await mocks.stop() + + class TestPowerDistributingActor: # pylint: disable=protected-access # pylint: disable=too-many-public-methods @@ -123,7 +139,7 @@ class TestPowerDistributingActor: async def _patch_battery_pool_status( self, - mocks: Mocks, + mocks: _Mocks, mocker: MockerFixture, battery_ids: abc.Set[int] | None = None, ) -> None: @@ -154,60 +170,63 @@ async def _patch_battery_pool_status( ) ) - async def test_constructor(self, mocker: MockerFixture) -> None: - """Test if gets all necessary data.""" - mockgrid = MockMicrogrid(grid_meter=True) + async def test_constructor_with_grid_meter(self, mocker: MockerFixture) -> None: + """Test the constructor works with a grid meter.""" + mockgrid = MockMicrogrid(grid_meter=True, mocker=mocker) mockgrid.add_batteries(2) mockgrid.add_batteries(1, no_meter=True) - await mockgrid.start(mocker) - requests_channel = Broadcast[Request]("power_distributor requests") - results_channel = Broadcast[Result]("power_distributor results") - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - results_sender=results_channel.new_sender(), - component_pool_status_sender=battery_status_channel.new_sender(), - ) as distributor: - assert isinstance(distributor._component_manager, BatteryManager) - assert distributor._component_manager._bat_invs_map == { - 9: {8}, - 19: {18}, - 29: {28}, - } - assert distributor._component_manager._inv_bats_map == { - 8: {9}, - 18: {19}, - 28: {29}, - } - await mockgrid.cleanup() - - # Test if it works without grid side meter - mockgrid = MockMicrogrid(grid_meter=False) + async with mockgrid: + requests_channel = Broadcast[Request]("power_distributor requests") + results_channel = Broadcast[Result]("power_distributor results") + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + results_sender=results_channel.new_sender(), + component_pool_status_sender=battery_status_channel.new_sender(), + ) as distributor: + assert isinstance(distributor._component_manager, BatteryManager) + assert distributor._component_manager._bat_invs_map == { + 9: {8}, + 19: {18}, + 29: {28}, + } + assert distributor._component_manager._inv_bats_map == { + 8: {9}, + 18: {19}, + 28: {29}, + } + + async def test_constructor_without_grid_meter(self, mocker: MockerFixture) -> None: + """Test the constructor works without a grid meter.""" + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_batteries(1) mockgrid.add_batteries(2, no_meter=True) - await mockgrid.start(mocker) - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - results_sender=results_channel.new_sender(), - component_pool_status_sender=battery_status_channel.new_sender(), - ) as distributor: - assert isinstance(distributor._component_manager, BatteryManager) - assert distributor._component_manager._bat_invs_map == { - 9: {8}, - 19: {18}, - 29: {28}, - } - assert distributor._component_manager._inv_bats_map == { - 8: {9}, - 18: {19}, - 28: {29}, - } - await mockgrid.cleanup() + + async with mockgrid: + requests_channel = Broadcast[Request]("power_distributor requests") + results_channel = Broadcast[Result]("power_distributor results") + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + results_sender=results_channel.new_sender(), + component_pool_status_sender=battery_status_channel.new_sender(), + ) as distributor: + assert isinstance(distributor._component_manager, BatteryManager) + assert distributor._component_manager._bat_invs_map == { + 9: {8}, + 19: {18}, + 29: {28}, + } + assert distributor._component_manager._inv_bats_map == { + 8: {9}, + 18: {19}, + 28: {29}, + } async def init_component_data( self, - mocks: Mocks, + mocks: _Mocks, *, skip_batteries: abc.Set[int] | None = None, skip_inverters: abc.Set[int] | None = None, @@ -237,7 +256,7 @@ async def init_component_data( async def test_power_distributor_one_user(self, mocker: MockerFixture) -> None: """Test if power distribution works with a single user.""" - mocks = await Mocks.new(mocker) + mocks = await _Mocks.new(mocker) requests_channel = Broadcast[Request]("power_distributor requests") results_channel = Broadcast[Result]("power_distributor results") @@ -280,92 +299,89 @@ async def test_power_distributor_exclusion_bounds( self, mocker: MockerFixture ) -> None: """Test if power distributing actor rejects non-zero requests in exclusion bounds.""" - mocks = await Mocks.new(mocker) - - await self._patch_battery_pool_status(mocks, mocker, {9, 19}) - await self.init_component_data(mocks, skip_batteries={9, 19}) - - mocks.streamer.start_streaming( - battery_msg( - 9, - soc=Metric(60, Bound(20, 80)), - capacity=Metric(98000), - power=PowerBounds(-1000, -300, 300, 1000), - ), - 0.05, - ) - - mocks.streamer.start_streaming( - battery_msg( - 19, - soc=Metric(60, Bound(20, 80)), - capacity=Metric(98000), - power=PowerBounds(-1000, -300, 300, 1000), - ), - 0.05, - ) - - requests_channel = Broadcast[Request]("power_distributor requests") - results_channel = Broadcast[Result]("power_distributor results") - - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - results_sender=results_channel.new_sender(), - component_pool_status_sender=battery_status_channel.new_sender(), - wait_for_data_sec=0.1, - ): - # zero power requests should pass through despite the exclusion bounds. - request = Request( - power=Power.zero(), - component_ids={9, 19}, - request_timeout=SAFETY_TIMEOUT, - ) - - await requests_channel.new_sender().send(request) - result_rx = results_channel.new_receiver() - - done, pending = await asyncio.wait( - [asyncio.create_task(result_rx.receive())], - timeout=SAFETY_TIMEOUT.total_seconds(), - ) - - assert len(pending) == 0 - assert len(done) == 1 - - result: Result = done.pop().result() - assert isinstance(result, Success) - assert result.succeeded_power.isclose(Power.zero(), abs_tol=1e-9) - assert result.excess_power.isclose(Power.zero(), abs_tol=1e-9) - assert result.request == request + async with _mocks(mocker) as mocks: + await self._patch_battery_pool_status(mocks, mocker, {9, 19}) + await self.init_component_data(mocks, skip_batteries={9, 19}) - # non-zero power requests that fall within the exclusion bounds should be - # rejected. - request = Request( - power=Power.from_watts(300.0), - component_ids={9, 19}, - request_timeout=SAFETY_TIMEOUT, + mocks.streamer.start_streaming( + battery_msg( + 9, + soc=Metric(60, Bound(20, 80)), + capacity=Metric(98000), + power=PowerBounds(-1000, -300, 300, 1000), + ), + 0.05, ) - await requests_channel.new_sender().send(request) - result_rx = results_channel.new_receiver() - - done, pending = await asyncio.wait( - [asyncio.create_task(result_rx.receive())], - timeout=SAFETY_TIMEOUT.total_seconds(), + mocks.streamer.start_streaming( + battery_msg( + 19, + soc=Metric(60, Bound(20, 80)), + capacity=Metric(98000), + power=PowerBounds(-1000, -300, 300, 1000), + ), + 0.05, ) - assert len(pending) == 0 - assert len(done) == 1 - - result = done.pop().result() - assert isinstance( - result, OutOfBounds - ), f"Expected OutOfBounds, got {result}" - assert result.bounds == PowerBounds(-1000, -600, 600, 1000) - assert result.request == request - - await mocks.stop() + requests_channel = Broadcast[Request]("power_distributor requests") + results_channel = Broadcast[Result]("power_distributor results") + + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + results_sender=results_channel.new_sender(), + component_pool_status_sender=battery_status_channel.new_sender(), + wait_for_data_sec=0.1, + ): + # zero power requests should pass through despite the exclusion bounds. + request = Request( + power=Power.zero(), + component_ids={9, 19}, + request_timeout=SAFETY_TIMEOUT, + ) + + await requests_channel.new_sender().send(request) + result_rx = results_channel.new_receiver() + + done, pending = await asyncio.wait( + [asyncio.create_task(result_rx.receive())], + timeout=SAFETY_TIMEOUT.total_seconds(), + ) + + assert len(pending) == 0 + assert len(done) == 1 + + result: Result = done.pop().result() + assert isinstance(result, Success) + assert result.succeeded_power.isclose(Power.zero(), abs_tol=1e-9) + assert result.excess_power.isclose(Power.zero(), abs_tol=1e-9) + assert result.request == request + + # non-zero power requests that fall within the exclusion bounds should be + # rejected. + request = Request( + power=Power.from_watts(300.0), + component_ids={9, 19}, + request_timeout=SAFETY_TIMEOUT, + ) + + await requests_channel.new_sender().send(request) + result_rx = results_channel.new_receiver() + + done, pending = await asyncio.wait( + [asyncio.create_task(result_rx.receive())], + timeout=SAFETY_TIMEOUT.total_seconds(), + ) + + assert len(pending) == 0 + assert len(done) == 1 + + result = done.pop().result() + assert isinstance( + result, OutOfBounds + ), f"Expected OutOfBounds, got {result}" + assert result.bounds == PowerBounds(-1000, -600, 600, 1000) + assert result.request == request # pylint: disable=too-many-locals async def test_two_batteries_one_inverters(self, mocker: MockerFixture) -> None: @@ -392,46 +408,47 @@ async def test_two_batteries_one_inverters(self, mocker: MockerFixture) -> None: ) ) - mocks = await Mocks.new(mocker, graph=graph) - await self.init_component_data(mocks) - - requests_channel = Broadcast[Request]("power_distributor requests") - results_channel = Broadcast[Result]("power_distributor results") - request = Request( - power=Power.from_watts(1200.0), - component_ids={bat_component1.component_id, bat_component2.component_id}, - request_timeout=SAFETY_TIMEOUT, - ) + async with _mocks(mocker, graph=graph) as mocks: + await self.init_component_data(mocks) - await self._patch_battery_pool_status(mocks, mocker, request.component_ids) + requests_channel = Broadcast[Request]("power_distributor requests") + results_channel = Broadcast[Result]("power_distributor results") + request = Request( + power=Power.from_watts(1200.0), + component_ids={ + bat_component1.component_id, + bat_component2.component_id, + }, + request_timeout=SAFETY_TIMEOUT, + ) - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + await self._patch_battery_pool_status(mocks, mocker, request.component_ids) - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - component_pool_status_sender=battery_status_channel.new_sender(), - results_sender=results_channel.new_sender(), - wait_for_data_sec=0.1, - ): - await requests_channel.new_sender().send(request) - result_rx = results_channel.new_receiver() + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") - done, pending = await asyncio.wait( - [asyncio.create_task(result_rx.receive())], - timeout=SAFETY_TIMEOUT.total_seconds(), - ) + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + component_pool_status_sender=battery_status_channel.new_sender(), + results_sender=results_channel.new_sender(), + wait_for_data_sec=0.1, + ): + await requests_channel.new_sender().send(request) + result_rx = results_channel.new_receiver() - assert len(pending) == 0 - assert len(done) == 1 + done, pending = await asyncio.wait( + [asyncio.create_task(result_rx.receive())], + timeout=SAFETY_TIMEOUT.total_seconds(), + ) - result: Result = done.pop().result() - assert isinstance(result, Success) - # Inverter bounded at 500 - assert result.succeeded_power.isclose(Power.from_watts(500.0)) - assert result.excess_power.isclose(Power.from_watts(700.0)) - assert result.request == request + assert len(pending) == 0 + assert len(done) == 1 - await mocks.stop() + result: Result = done.pop().result() + assert isinstance(result, Success) + # Inverter bounded at 500 + assert result.succeeded_power.isclose(Power.from_watts(500.0)) + assert result.excess_power.isclose(Power.from_watts(700.0)) + assert result.request == request async def test_two_batteries_one_broken_one_inverters( self, mocker: MockerFixture @@ -458,59 +475,58 @@ async def test_two_batteries_one_broken_one_inverters( ) ) - mocks = await Mocks.new(mocker, graph=graph) - await self.init_component_data( - mocks, skip_batteries={bat_components[0].component_id} - ) - - mocks.streamer.start_streaming( - battery_msg( - bat_components[0].component_id, - soc=Metric(math.nan, Bound(20, 80)), - capacity=Metric(98000), - power=PowerBounds(-1000, 0, 0, 1000), - ), - 0.05, - ) + async with _mocks(mocker, graph=graph) as mocks: + await self.init_component_data( + mocks, skip_batteries={bat_components[0].component_id} + ) - requests_channel = Broadcast[Request]("power_distributor") - results_channel = Broadcast[Result]("power_distributor results") + mocks.streamer.start_streaming( + battery_msg( + bat_components[0].component_id, + soc=Metric(math.nan, Bound(20, 80)), + capacity=Metric(98000), + power=PowerBounds(-1000, 0, 0, 1000), + ), + 0.05, + ) - request = Request( - power=Power.from_watts(1200.0), - component_ids=set(battery.component_id for battery in bat_components), - request_timeout=SAFETY_TIMEOUT, - ) + requests_channel = Broadcast[Request]("power_distributor") + results_channel = Broadcast[Result]("power_distributor results") - await self._patch_battery_pool_status(mocks, mocker, request.component_ids) - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + request = Request( + power=Power.from_watts(1200.0), + component_ids=set(battery.component_id for battery in bat_components), + request_timeout=SAFETY_TIMEOUT, + ) - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - component_pool_status_sender=battery_status_channel.new_sender(), - results_sender=results_channel.new_sender(), - wait_for_data_sec=0.1, - ): - await requests_channel.new_sender().send(request) - result_rx = results_channel.new_receiver() + await self._patch_battery_pool_status(mocks, mocker, request.component_ids) + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") - done, pending = await asyncio.wait( - [asyncio.create_task(result_rx.receive())], - timeout=SAFETY_TIMEOUT.total_seconds(), - ) + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + component_pool_status_sender=battery_status_channel.new_sender(), + results_sender=results_channel.new_sender(), + wait_for_data_sec=0.1, + ): + await requests_channel.new_sender().send(request) + result_rx = results_channel.new_receiver() - assert len(pending) == 0 - assert len(done) == 1 + done, pending = await asyncio.wait( + [asyncio.create_task(result_rx.receive())], + timeout=SAFETY_TIMEOUT.total_seconds(), + ) - result: Result = done.pop().result() + assert len(pending) == 0 + assert len(done) == 1 - assert isinstance(result, Error) - assert result.request == request - assert ( - result.msg == "No data for at least one of the given batteries {9, 19}" - ) + result: Result = done.pop().result() - await mocks.stop() + assert isinstance(result, Error) + assert result.request == request + assert ( + result.msg + == "No data for at least one of the given batteries {9, 19}" + ) async def test_battery_two_inverters(self, mocker: MockerFixture) -> None: """Test if power distribution works with two inverters for one battery.""" @@ -537,46 +553,44 @@ async def test_battery_two_inverters(self, mocker: MockerFixture) -> None: ) ) - mocks = await Mocks.new(mocker, graph=graph) - await self.init_component_data(mocks) - - requests_channel = Broadcast[Request]("power_distributor requests") - results_channel = Broadcast[Result]("power_distributor results") - - request = Request( - power=Power.from_watts(1200.0), - component_ids={bat_component.component_id}, - request_timeout=SAFETY_TIMEOUT, - ) - - await self._patch_battery_pool_status(mocks, mocker, request.component_ids) - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + async with _mocks(mocker, graph=graph) as mocks: + await self.init_component_data(mocks) - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - component_pool_status_sender=battery_status_channel.new_sender(), - results_sender=results_channel.new_sender(), - wait_for_data_sec=0.1, - ): - await requests_channel.new_sender().send(request) - result_rx = results_channel.new_receiver() + requests_channel = Broadcast[Request]("power_distributor requests") + results_channel = Broadcast[Result]("power_distributor results") - done, pending = await asyncio.wait( - [asyncio.create_task(result_rx.receive())], - timeout=SAFETY_TIMEOUT.total_seconds(), + request = Request( + power=Power.from_watts(1200.0), + component_ids={bat_component.component_id}, + request_timeout=SAFETY_TIMEOUT, ) - assert len(pending) == 0 - assert len(done) == 1 - - result: Result = done.pop().result() - assert isinstance(result, Success) - # Inverters each bounded at 500, together 1000 - assert result.succeeded_power.isclose(Power.from_watts(1000.0)) - assert result.excess_power.isclose(Power.from_watts(200.0)) - assert result.request == request - - await mocks.stop() + await self._patch_battery_pool_status(mocks, mocker, request.component_ids) + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + component_pool_status_sender=battery_status_channel.new_sender(), + results_sender=results_channel.new_sender(), + wait_for_data_sec=0.1, + ): + await requests_channel.new_sender().send(request) + result_rx = results_channel.new_receiver() + + done, pending = await asyncio.wait( + [asyncio.create_task(result_rx.receive())], + timeout=SAFETY_TIMEOUT.total_seconds(), + ) + + assert len(pending) == 0 + assert len(done) == 1 + + result: Result = done.pop().result() + assert isinstance(result, Success) + # Inverters each bounded at 500, together 1000 + assert result.succeeded_power.isclose(Power.from_watts(1000.0)) + assert result.excess_power.isclose(Power.from_watts(200.0)) + assert result.request == request async def test_two_batteries_three_inverters(self, mocker: MockerFixture) -> None: """Test if power distribution works with two batteries connected to three inverters.""" @@ -608,46 +622,44 @@ async def test_two_batteries_three_inverters(self, mocker: MockerFixture) -> Non ) ) - mocks = await Mocks.new(mocker, graph=graph) - await self.init_component_data(mocks) - - requests_channel = Broadcast[Request]("power_distributor requests") - results_channel = Broadcast[Result]("power_distributor results") - - request = Request( - power=Power.from_watts(1700.0), - component_ids={batteries[0].component_id, batteries[1].component_id}, - request_timeout=SAFETY_TIMEOUT, - ) - - await self._patch_battery_pool_status(mocks, mocker, request.component_ids) - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + async with _mocks(mocker, graph=graph) as mocks: + await self.init_component_data(mocks) - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - component_pool_status_sender=battery_status_channel.new_sender(), - results_sender=results_channel.new_sender(), - wait_for_data_sec=0.1, - ): - await requests_channel.new_sender().send(request) - result_rx = results_channel.new_receiver() + requests_channel = Broadcast[Request]("power_distributor requests") + results_channel = Broadcast[Result]("power_distributor results") - done, pending = await asyncio.wait( - [asyncio.create_task(result_rx.receive())], - timeout=SAFETY_TIMEOUT.total_seconds(), + request = Request( + power=Power.from_watts(1700.0), + component_ids={batteries[0].component_id, batteries[1].component_id}, + request_timeout=SAFETY_TIMEOUT, ) - assert len(pending) == 0 - assert len(done) == 1 - - result: Result = done.pop().result() - assert isinstance(result, Success) - # each inverter is bounded at 500 and we have 3 inverters - assert result.succeeded_power.isclose(Power.from_watts(1500.0)) - assert result.excess_power.isclose(Power.from_watts(200.0)) - assert result.request == request - - await mocks.stop() + await self._patch_battery_pool_status(mocks, mocker, request.component_ids) + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + component_pool_status_sender=battery_status_channel.new_sender(), + results_sender=results_channel.new_sender(), + wait_for_data_sec=0.1, + ): + await requests_channel.new_sender().send(request) + result_rx = results_channel.new_receiver() + + done, pending = await asyncio.wait( + [asyncio.create_task(result_rx.receive())], + timeout=SAFETY_TIMEOUT.total_seconds(), + ) + + assert len(pending) == 0 + assert len(done) == 1 + + result: Result = done.pop().result() + assert isinstance(result, Success) + # each inverter is bounded at 500 and we have 3 inverters + assert result.succeeded_power.isclose(Power.from_watts(1500.0)) + assert result.excess_power.isclose(Power.from_watts(200.0)) + assert result.request == request async def test_two_batteries_one_inverter_different_exclusion_bounds_2( self, mocker: MockerFixture @@ -668,69 +680,66 @@ async def test_two_batteries_one_inverter_different_exclusion_bounds_2( ) ) - mocks = await Mocks.new(mocker, graph=graph) + async with _mocks(mocker, graph=graph) as mocks: + mocks.streamer.start_streaming( + inverter_msg( + inverter.component_id, + power=PowerBounds(-1000, -500, 500, 1000), + ), + 0.05, + ) + mocks.streamer.start_streaming( + battery_msg( + batteries[0].component_id, + soc=Metric(40, Bound(20, 80)), + capacity=Metric(10_000), + power=PowerBounds(-1000, -200, 200, 1000), + ), + 0.05, + ) + mocks.streamer.start_streaming( + battery_msg( + batteries[1].component_id, + soc=Metric(40, Bound(20, 80)), + capacity=Metric(10_000), + power=PowerBounds(-1000, -100, 100, 1000), + ), + 0.05, + ) - mocks.streamer.start_streaming( - inverter_msg( - inverter.component_id, - power=PowerBounds(-1000, -500, 500, 1000), - ), - 0.05, - ) - mocks.streamer.start_streaming( - battery_msg( - batteries[0].component_id, - soc=Metric(40, Bound(20, 80)), - capacity=Metric(10_000), - power=PowerBounds(-1000, -200, 200, 1000), - ), - 0.05, - ) - mocks.streamer.start_streaming( - battery_msg( - batteries[1].component_id, - soc=Metric(40, Bound(20, 80)), - capacity=Metric(10_000), - power=PowerBounds(-1000, -100, 100, 1000), - ), - 0.05, - ) + requests_channel = Broadcast[Request]("power_distributor requests") + results_channel = Broadcast[Result]("power_distributor results") - requests_channel = Broadcast[Request]("power_distributor requests") - results_channel = Broadcast[Result]("power_distributor results") + request = Request( + power=Power.from_watts(300.0), + component_ids={batteries[0].component_id, batteries[1].component_id}, + request_timeout=SAFETY_TIMEOUT, + ) - request = Request( - power=Power.from_watts(300.0), - component_ids={batteries[0].component_id, batteries[1].component_id}, - request_timeout=SAFETY_TIMEOUT, - ) + await self._patch_battery_pool_status(mocks, mocker, request.component_ids) + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") - await self._patch_battery_pool_status(mocks, mocker, request.component_ids) - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + component_pool_status_sender=battery_status_channel.new_sender(), + results_sender=results_channel.new_sender(), + wait_for_data_sec=0.1, + ): + await requests_channel.new_sender().send(request) + result_rx = results_channel.new_receiver() - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - component_pool_status_sender=battery_status_channel.new_sender(), - results_sender=results_channel.new_sender(), - wait_for_data_sec=0.1, - ): - await requests_channel.new_sender().send(request) - result_rx = results_channel.new_receiver() + done, pending = await asyncio.wait( + [asyncio.create_task(result_rx.receive())], + timeout=SAFETY_TIMEOUT.total_seconds(), + ) - done, pending = await asyncio.wait( - [asyncio.create_task(result_rx.receive())], - timeout=SAFETY_TIMEOUT.total_seconds(), - ) + assert len(pending) == 0 + assert len(done) == 1 - assert len(pending) == 0 - assert len(done) == 1 - - result: Result = done.pop().result() - assert isinstance(result, OutOfBounds) - assert result.request == request - assert result.bounds == PowerBounds(-1000, -500, 500, 1000) - - await mocks.stop() + result: Result = done.pop().result() + assert isinstance(result, OutOfBounds) + assert result.request == request + assert result.bounds == PowerBounds(-1000, -500, 500, 1000) async def test_two_batteries_one_inverter_different_exclusion_bounds( self, mocker: MockerFixture @@ -758,65 +767,63 @@ async def test_two_batteries_one_inverter_different_exclusion_bounds( ) ) - mocks = await Mocks.new(mocker, graph=graph) - await self.init_component_data( - mocks, skip_batteries={bat.component_id for bat in batteries} - ) - mocks.streamer.start_streaming( - battery_msg( - batteries[0].component_id, - soc=Metric(40, Bound(20, 80)), - capacity=Metric(10_000), - power=PowerBounds(-1000, -200, 200, 1000), - ), - 0.05, - ) - mocks.streamer.start_streaming( - battery_msg( - batteries[1].component_id, - soc=Metric(40, Bound(20, 80)), - capacity=Metric(10_000), - power=PowerBounds(-1000, -100, 100, 1000), - ), - 0.05, - ) + async with _mocks(mocker, graph=graph) as mocks: + await self.init_component_data( + mocks, skip_batteries={bat.component_id for bat in batteries} + ) + mocks.streamer.start_streaming( + battery_msg( + batteries[0].component_id, + soc=Metric(40, Bound(20, 80)), + capacity=Metric(10_000), + power=PowerBounds(-1000, -200, 200, 1000), + ), + 0.05, + ) + mocks.streamer.start_streaming( + battery_msg( + batteries[1].component_id, + soc=Metric(40, Bound(20, 80)), + capacity=Metric(10_000), + power=PowerBounds(-1000, -100, 100, 1000), + ), + 0.05, + ) - requests_channel = Broadcast[Request]("power_distributor requests") - results_channel = Broadcast[Result]("power_distributor results") + requests_channel = Broadcast[Request]("power_distributor requests") + results_channel = Broadcast[Result]("power_distributor results") - request = Request( - power=Power.from_watts(300.0), - component_ids={batteries[0].component_id, batteries[1].component_id}, - request_timeout=SAFETY_TIMEOUT, - ) - - await self._patch_battery_pool_status(mocks, mocker, request.component_ids) - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + request = Request( + power=Power.from_watts(300.0), + component_ids={batteries[0].component_id, batteries[1].component_id}, + request_timeout=SAFETY_TIMEOUT, + ) - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - component_pool_status_sender=battery_status_channel.new_sender(), - results_sender=results_channel.new_sender(), - wait_for_data_sec=0.1, - ): - await requests_channel.new_sender().send(request) - result_rx = results_channel.new_receiver() + await self._patch_battery_pool_status(mocks, mocker, request.component_ids) + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") - done, pending = await asyncio.wait( - [asyncio.create_task(result_rx.receive())], - timeout=SAFETY_TIMEOUT.total_seconds(), - ) + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + component_pool_status_sender=battery_status_channel.new_sender(), + results_sender=results_channel.new_sender(), + wait_for_data_sec=0.1, + ): + await requests_channel.new_sender().send(request) + result_rx = results_channel.new_receiver() - assert len(pending) == 0 - assert len(done) == 1 + done, pending = await asyncio.wait( + [asyncio.create_task(result_rx.receive())], + timeout=SAFETY_TIMEOUT.total_seconds(), + ) - result: Result = done.pop().result() - assert isinstance(result, OutOfBounds) - assert result.request == request - # each inverter is bounded at 500 - assert result.bounds == PowerBounds(-500, -400, 400, 500) + assert len(pending) == 0 + assert len(done) == 1 - await mocks.stop() + result: Result = done.pop().result() + assert isinstance(result, OutOfBounds) + assert result.request == request + # each inverter is bounded at 500 + assert result.bounds == PowerBounds(-500, -400, 400, 500) async def test_connected_but_not_requested_batteries( self, mocker: MockerFixture @@ -842,496 +849,477 @@ async def test_connected_but_not_requested_batteries( ) ) - mocks = await Mocks.new(mocker, graph=graph) - await self.init_component_data(mocks) + async with _mocks(mocker, graph=graph) as mocks: + await self.init_component_data(mocks) - requests_channel = Broadcast[Request]("power_distributor requests") - results_channel = Broadcast[Result]("power_distributor results") + requests_channel = Broadcast[Request]("power_distributor requests") + results_channel = Broadcast[Result]("power_distributor results") - request = Request( - power=Power.from_watts(600.0), - component_ids={batteries[0].component_id}, - request_timeout=SAFETY_TIMEOUT, - ) - - await self._patch_battery_pool_status(mocks, mocker, request.component_ids) - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") - - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - component_pool_status_sender=battery_status_channel.new_sender(), - results_sender=results_channel.new_sender(), - wait_for_data_sec=0.1, - ): - await requests_channel.new_sender().send(request) - result_rx = results_channel.new_receiver() - - done, pending = await asyncio.wait( - [asyncio.create_task(result_rx.receive())], - timeout=SAFETY_TIMEOUT.total_seconds(), - ) - - assert len(pending) == 0 - assert len(done) == 1 - - result: Result = done.pop().result() - assert isinstance(result, Error) - assert result.request == request - - err_msg = re.search( - r"'Inverters \{48\} are connected to batteries that were not requested: \{19\}'", - result.msg, + request = Request( + power=Power.from_watts(600.0), + component_ids={batteries[0].component_id}, + request_timeout=SAFETY_TIMEOUT, ) - assert err_msg is not None - await mocks.stop() + await self._patch_battery_pool_status(mocks, mocker, request.component_ids) + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + component_pool_status_sender=battery_status_channel.new_sender(), + results_sender=results_channel.new_sender(), + wait_for_data_sec=0.1, + ): + await requests_channel.new_sender().send(request) + result_rx = results_channel.new_receiver() + + done, pending = await asyncio.wait( + [asyncio.create_task(result_rx.receive())], + timeout=SAFETY_TIMEOUT.total_seconds(), + ) + + assert len(pending) == 0 + assert len(done) == 1 + + result: Result = done.pop().result() + assert isinstance(result, Error) + assert result.request == request + + err_msg = re.search( + r"'Inverters \{48\} are connected to batteries that were not " + r"requested: \{19\}'", + result.msg, + ) + assert err_msg is not None async def test_battery_soc_nan(self, mocker: MockerFixture) -> None: """Test if battery with SoC==NaN is not used.""" - mocks = await Mocks.new(mocker, grid_meter=False) - await self.init_component_data(mocks, skip_batteries={9}) - - mocks.streamer.start_streaming( - battery_msg( - 9, - soc=Metric(math.nan, Bound(20, 80)), - capacity=Metric(98000), - power=PowerBounds(-1000, 0, 0, 1000), - ), - 0.05, - ) - - requests_channel = Broadcast[Request]("power_distributor requests") - results_channel = Broadcast[Result]("power_distributor results") + async with _mocks(mocker, grid_meter=False) as mocks: + await self.init_component_data(mocks, skip_batteries={9}) - request = Request( - power=Power.from_kilowatts(1.2), - component_ids={9, 19}, - request_timeout=SAFETY_TIMEOUT, - ) + mocks.streamer.start_streaming( + battery_msg( + 9, + soc=Metric(math.nan, Bound(20, 80)), + capacity=Metric(98000), + power=PowerBounds(-1000, 0, 0, 1000), + ), + 0.05, + ) - await self._patch_battery_pool_status(mocks, mocker, request.component_ids) - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - results_sender=results_channel.new_sender(), - component_pool_status_sender=battery_status_channel.new_sender(), - wait_for_data_sec=0.1, - ): - await requests_channel.new_sender().send(request) - result_rx = results_channel.new_receiver() + requests_channel = Broadcast[Request]("power_distributor requests") + results_channel = Broadcast[Result]("power_distributor results") - done, pending = await asyncio.wait( - [asyncio.create_task(result_rx.receive())], - timeout=SAFETY_TIMEOUT.total_seconds(), + request = Request( + power=Power.from_kilowatts(1.2), + component_ids={9, 19}, + request_timeout=SAFETY_TIMEOUT, ) - assert len(pending) == 0 - assert len(done) == 1 + await self._patch_battery_pool_status(mocks, mocker, request.component_ids) + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + results_sender=results_channel.new_sender(), + component_pool_status_sender=battery_status_channel.new_sender(), + wait_for_data_sec=0.1, + ): + await requests_channel.new_sender().send(request) + result_rx = results_channel.new_receiver() + + done, pending = await asyncio.wait( + [asyncio.create_task(result_rx.receive())], + timeout=SAFETY_TIMEOUT.total_seconds(), + ) - result: Result = done.pop().result() - assert isinstance(result, Success) - assert result.succeeded_components == {19} - assert result.succeeded_power.isclose(Power.from_watts(500.0)) - assert result.excess_power.isclose(Power.from_watts(700.0)) - assert result.request == request + assert len(pending) == 0 + assert len(done) == 1 - await mocks.stop() + result: Result = done.pop().result() + assert isinstance(result, Success) + assert result.succeeded_components == {19} + assert result.succeeded_power.isclose(Power.from_watts(500.0)) + assert result.excess_power.isclose(Power.from_watts(700.0)) + assert result.request == request async def test_battery_capacity_nan(self, mocker: MockerFixture) -> None: """Test battery with capacity set to NaN is not used.""" - mocks = await Mocks.new(mocker, grid_meter=False) - await self.init_component_data(mocks, skip_batteries={9}) - - mocks.streamer.start_streaming( - battery_msg( - 9, - soc=Metric(40, Bound(20, 80)), - capacity=Metric(math.nan), - power=PowerBounds(-1000, 0, 0, 1000), - ), - 0.05, - ) + async with _mocks(mocker, grid_meter=False) as mocks: + await self.init_component_data(mocks, skip_batteries={9}) - requests_channel = Broadcast[Request]("power_distributor requests") - results_channel = Broadcast[Result]("power_distributor results") + mocks.streamer.start_streaming( + battery_msg( + 9, + soc=Metric(40, Bound(20, 80)), + capacity=Metric(math.nan), + power=PowerBounds(-1000, 0, 0, 1000), + ), + 0.05, + ) - request = Request( - power=Power.from_kilowatts(1.2), - component_ids={9, 19}, - request_timeout=SAFETY_TIMEOUT, - ) + requests_channel = Broadcast[Request]("power_distributor requests") + results_channel = Broadcast[Result]("power_distributor results") - await self._patch_battery_pool_status(mocks, mocker, request.component_ids) + request = Request( + power=Power.from_kilowatts(1.2), + component_ids={9, 19}, + request_timeout=SAFETY_TIMEOUT, + ) - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - results_sender=results_channel.new_sender(), - component_pool_status_sender=battery_status_channel.new_sender(), - wait_for_data_sec=0.1, - ): - await requests_channel.new_sender().send(request) - result_rx = results_channel.new_receiver() + await self._patch_battery_pool_status(mocks, mocker, request.component_ids) - done, pending = await asyncio.wait( - [asyncio.create_task(result_rx.receive())], - timeout=SAFETY_TIMEOUT.total_seconds(), - ) + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + results_sender=results_channel.new_sender(), + component_pool_status_sender=battery_status_channel.new_sender(), + wait_for_data_sec=0.1, + ): + await requests_channel.new_sender().send(request) + result_rx = results_channel.new_receiver() - assert len(pending) == 0 - assert len(done) == 1 + done, pending = await asyncio.wait( + [asyncio.create_task(result_rx.receive())], + timeout=SAFETY_TIMEOUT.total_seconds(), + ) - result: Result = done.pop().result() - assert isinstance(result, Success) - assert result.succeeded_components == {19} - assert result.succeeded_power.isclose(Power.from_watts(500.0)) - assert result.excess_power.isclose(Power.from_watts(700.0)) - assert result.request == request + assert len(pending) == 0 + assert len(done) == 1 - await mocks.stop() + result: Result = done.pop().result() + assert isinstance(result, Success) + assert result.succeeded_components == {19} + assert result.succeeded_power.isclose(Power.from_watts(500.0)) + assert result.excess_power.isclose(Power.from_watts(700.0)) + assert result.request == request async def test_battery_power_bounds_nan(self, mocker: MockerFixture) -> None: """Test battery with power bounds set to NaN is not used.""" - mocks = await Mocks.new(mocker, grid_meter=False) - await self.init_component_data( - mocks, skip_batteries={9}, skip_inverters={8, 18} - ) + async with _mocks(mocker, grid_meter=False) as mocks: + await self.init_component_data( + mocks, skip_batteries={9}, skip_inverters={8, 18} + ) - mocks.streamer.start_streaming( - inverter_msg( - 18, - power=PowerBounds(-1000, 0, 0, 1000), - ), - 0.05, - ) + mocks.streamer.start_streaming( + inverter_msg( + 18, + power=PowerBounds(-1000, 0, 0, 1000), + ), + 0.05, + ) - # Battery 9 should not work because both battery and inverter sends NaN - mocks.streamer.start_streaming( - inverter_msg( - 8, - power=PowerBounds(-1000, 0, 0, math.nan), - ), - 0.05, - ) + # Battery 9 should not work because both battery and inverter sends NaN + mocks.streamer.start_streaming( + inverter_msg( + 8, + power=PowerBounds(-1000, 0, 0, math.nan), + ), + 0.05, + ) - mocks.streamer.start_streaming( - battery_msg( - 9, - soc=Metric(40, Bound(20, 80)), - capacity=Metric(float(98000)), - power=PowerBounds(math.nan, 0, 0, math.nan), - ), - 0.05, - ) + mocks.streamer.start_streaming( + battery_msg( + 9, + soc=Metric(40, Bound(20, 80)), + capacity=Metric(float(98000)), + power=PowerBounds(math.nan, 0, 0, math.nan), + ), + 0.05, + ) - requests_channel = Broadcast[Request]("power_distributor requests") - results_channel = Broadcast[Result]("power_distributor results") + requests_channel = Broadcast[Request]("power_distributor requests") + results_channel = Broadcast[Result]("power_distributor results") - request = Request( - power=Power.from_kilowatts(1.2), - component_ids={9, 19}, - request_timeout=SAFETY_TIMEOUT, - ) + request = Request( + power=Power.from_kilowatts(1.2), + component_ids={9, 19}, + request_timeout=SAFETY_TIMEOUT, + ) - await self._patch_battery_pool_status(mocks, mocker, request.component_ids) + await self._patch_battery_pool_status(mocks, mocker, request.component_ids) - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - results_sender=results_channel.new_sender(), - component_pool_status_sender=battery_status_channel.new_sender(), - wait_for_data_sec=0.1, - ): - await requests_channel.new_sender().send(request) - result_rx = results_channel.new_receiver() + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + results_sender=results_channel.new_sender(), + component_pool_status_sender=battery_status_channel.new_sender(), + wait_for_data_sec=0.1, + ): + await requests_channel.new_sender().send(request) + result_rx = results_channel.new_receiver() - done, pending = await asyncio.wait( - [asyncio.create_task(result_rx.receive())], - timeout=SAFETY_TIMEOUT.total_seconds(), - ) + done, pending = await asyncio.wait( + [asyncio.create_task(result_rx.receive())], + timeout=SAFETY_TIMEOUT.total_seconds(), + ) - assert len(pending) == 0 - assert len(done) == 1 - - result: Result = done.pop().result() - assert isinstance(result, Success) - assert result.succeeded_components == {19} - assert result.succeeded_power.isclose(Power.from_kilowatts(1.0)) - assert result.excess_power.isclose(Power.from_watts(200.0)) - assert result.request == request + assert len(pending) == 0 + assert len(done) == 1 - await mocks.stop() + result: Result = done.pop().result() + assert isinstance(result, Success) + assert result.succeeded_components == {19} + assert result.succeeded_power.isclose(Power.from_kilowatts(1.0)) + assert result.excess_power.isclose(Power.from_watts(200.0)) + assert result.request == request async def test_power_distributor_invalid_battery_id( self, mocker: MockerFixture ) -> None: """Test if power distribution raises error if any battery id is invalid.""" - mocks = await Mocks.new(mocker, grid_meter=False) - await self.init_component_data(mocks) + async with _mocks(mocker, grid_meter=False) as mocks: + await self.init_component_data(mocks) - requests_channel = Broadcast[Request]("power_distributor requests") - results_channel = Broadcast[Result]("power_distributor results") - request = Request( - power=Power.from_kilowatts(1.2), - component_ids={9, 100}, - request_timeout=SAFETY_TIMEOUT, - ) - - await self._patch_battery_pool_status(mocks, mocker, request.component_ids) + requests_channel = Broadcast[Request]("power_distributor requests") + results_channel = Broadcast[Result]("power_distributor results") + request = Request( + power=Power.from_kilowatts(1.2), + component_ids={9, 100}, + request_timeout=SAFETY_TIMEOUT, + ) - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - results_sender=results_channel.new_sender(), - component_pool_status_sender=battery_status_channel.new_sender(), - wait_for_data_sec=0.1, - ): - await requests_channel.new_sender().send(request) - result_rx = results_channel.new_receiver() + await self._patch_battery_pool_status(mocks, mocker, request.component_ids) - done, _ = await asyncio.wait( - [asyncio.create_task(result_rx.receive())], - timeout=SAFETY_TIMEOUT.total_seconds(), - ) + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + results_sender=results_channel.new_sender(), + component_pool_status_sender=battery_status_channel.new_sender(), + wait_for_data_sec=0.1, + ): + await requests_channel.new_sender().send(request) + result_rx = results_channel.new_receiver() - assert len(done) == 1 - result: Result = done.pop().result() - assert isinstance(result, Error) - assert result.request == request - err_msg = re.search(r"No battery 100, available batteries:", result.msg) - assert err_msg is not None + done, _ = await asyncio.wait( + [asyncio.create_task(result_rx.receive())], + timeout=SAFETY_TIMEOUT.total_seconds(), + ) - await mocks.stop() + assert len(done) == 1 + result: Result = done.pop().result() + assert isinstance(result, Error) + assert result.request == request + err_msg = re.search(r"No battery 100, available batteries:", result.msg) + assert err_msg is not None async def test_power_distributor_one_user_adjust_power_consume( self, mocker: MockerFixture ) -> None: """Test if power distribution works with single user works.""" - mocks = await Mocks.new(mocker, grid_meter=False) - await self.init_component_data(mocks) + async with _mocks(mocker, grid_meter=False) as mocks: + await self.init_component_data(mocks) - requests_channel = Broadcast[Request]("power_distributor") - results_channel = Broadcast[Result]("power_distributor results") - - request = Request( - power=Power.from_kilowatts(1.2), - component_ids={9, 19}, - request_timeout=SAFETY_TIMEOUT, - adjust_power=False, - ) + requests_channel = Broadcast[Request]("power_distributor") + results_channel = Broadcast[Result]("power_distributor results") - await self._patch_battery_pool_status(mocks, mocker, request.component_ids) + request = Request( + power=Power.from_kilowatts(1.2), + component_ids={9, 19}, + request_timeout=SAFETY_TIMEOUT, + adjust_power=False, + ) - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - results_sender=results_channel.new_sender(), - component_pool_status_sender=battery_status_channel.new_sender(), - wait_for_data_sec=0.1, - ): - await requests_channel.new_sender().send(request) - result_rx = results_channel.new_receiver() + await self._patch_battery_pool_status(mocks, mocker, request.component_ids) - done, pending = await asyncio.wait( - [asyncio.create_task(result_rx.receive())], - timeout=SAFETY_TIMEOUT.total_seconds(), - ) + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + results_sender=results_channel.new_sender(), + component_pool_status_sender=battery_status_channel.new_sender(), + wait_for_data_sec=0.1, + ): + await requests_channel.new_sender().send(request) + result_rx = results_channel.new_receiver() - assert len(pending) == 0 - assert len(done) == 1 + done, pending = await asyncio.wait( + [asyncio.create_task(result_rx.receive())], + timeout=SAFETY_TIMEOUT.total_seconds(), + ) - result = done.pop().result() - assert isinstance(result, OutOfBounds) - assert result is not None - assert result.request == request - assert result.bounds.inclusion_upper == 1000 + assert len(pending) == 0 + assert len(done) == 1 - await mocks.stop() + result = done.pop().result() + assert isinstance(result, OutOfBounds) + assert result is not None + assert result.request == request + assert result.bounds.inclusion_upper == 1000 async def test_power_distributor_one_user_adjust_power_supply( self, mocker: MockerFixture ) -> None: """Test if power distribution works with single user works.""" - mocks = await Mocks.new(mocker, grid_meter=False) - await self.init_component_data(mocks) + async with _mocks(mocker, grid_meter=False) as mocks: + await self.init_component_data(mocks) - requests_channel = Broadcast[Request]("power_distributor requests") - results_channel = Broadcast[Result]("power_distributor results") + requests_channel = Broadcast[Request]("power_distributor requests") + results_channel = Broadcast[Result]("power_distributor results") - request = Request( - power=-Power.from_kilowatts(1.2), - component_ids={9, 19}, - request_timeout=SAFETY_TIMEOUT, - adjust_power=False, - ) + request = Request( + power=-Power.from_kilowatts(1.2), + component_ids={9, 19}, + request_timeout=SAFETY_TIMEOUT, + adjust_power=False, + ) - await self._patch_battery_pool_status(mocks, mocker, request.component_ids) + await self._patch_battery_pool_status(mocks, mocker, request.component_ids) - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - results_sender=results_channel.new_sender(), - component_pool_status_sender=battery_status_channel.new_sender(), - wait_for_data_sec=0.1, - ): - await requests_channel.new_sender().send(request) - result_rx = results_channel.new_receiver() + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + results_sender=results_channel.new_sender(), + component_pool_status_sender=battery_status_channel.new_sender(), + wait_for_data_sec=0.1, + ): + await requests_channel.new_sender().send(request) + result_rx = results_channel.new_receiver() - done, pending = await asyncio.wait( - [asyncio.create_task(result_rx.receive())], - timeout=SAFETY_TIMEOUT.total_seconds(), - ) - - assert len(pending) == 0 - assert len(done) == 1 + done, pending = await asyncio.wait( + [asyncio.create_task(result_rx.receive())], + timeout=SAFETY_TIMEOUT.total_seconds(), + ) - result = done.pop().result() - assert isinstance(result, OutOfBounds) - assert result is not None - assert result.request == request - assert result.bounds.inclusion_lower == -1000 + assert len(pending) == 0 + assert len(done) == 1 - await mocks.stop() + result = done.pop().result() + assert isinstance(result, OutOfBounds) + assert result is not None + assert result.request == request + assert result.bounds.inclusion_lower == -1000 async def test_power_distributor_one_user_adjust_power_success( self, mocker: MockerFixture ) -> None: """Test if power distribution works with single user works.""" - mocks = await Mocks.new(mocker, grid_meter=False) - await self.init_component_data(mocks) - - requests_channel = Broadcast[Request]("power_distributor requests") - results_channel = Broadcast[Result]("power_distributor results") - - request = Request( - power=Power.from_kilowatts(1.0), - component_ids={9, 19}, - request_timeout=SAFETY_TIMEOUT, - adjust_power=False, - ) - - await self._patch_battery_pool_status(mocks, mocker, request.component_ids) - - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - results_sender=results_channel.new_sender(), - component_pool_status_sender=battery_status_channel.new_sender(), - wait_for_data_sec=0.1, - ): - await requests_channel.new_sender().send(request) - result_rx = results_channel.new_receiver() - - done, pending = await asyncio.wait( - [asyncio.create_task(result_rx.receive())], - timeout=SAFETY_TIMEOUT.total_seconds(), - ) - - assert len(pending) == 0 - assert len(done) == 1 - - result = done.pop().result() - assert isinstance(result, Success) - assert result.succeeded_power.isclose(Power.from_kilowatts(1.0)) - assert result.excess_power.isclose(Power.zero(), abs_tol=1e-9) - assert result.request == request - - await mocks.stop() + async with _mocks(mocker, grid_meter=False) as mocks: + await self.init_component_data(mocks) - async def test_not_all_batteries_are_working(self, mocker: MockerFixture) -> None: - """Test if power distribution works if not all batteries are working.""" - mocks = await Mocks.new(mocker, grid_meter=False) - await self.init_component_data(mocks) - - batteries = {9, 19} - - await self._patch_battery_pool_status(mocks, mocker, batteries - {9}) - - requests_channel = Broadcast[Request]("power_distributor requests") - results_channel = Broadcast[Result]("power_distributor results") + requests_channel = Broadcast[Request]("power_distributor requests") + results_channel = Broadcast[Result]("power_distributor results") - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - results_sender=results_channel.new_sender(), - component_pool_status_sender=battery_status_channel.new_sender(), - wait_for_data_sec=0.1, - ): request = Request( - power=Power.from_kilowatts(1.2), - component_ids=batteries, + power=Power.from_kilowatts(1.0), + component_ids={9, 19}, request_timeout=SAFETY_TIMEOUT, + adjust_power=False, ) - await requests_channel.new_sender().send(request) - result_rx = results_channel.new_receiver() + await self._patch_battery_pool_status(mocks, mocker, request.component_ids) - done, pending = await asyncio.wait( - [asyncio.create_task(result_rx.receive())], - timeout=SAFETY_TIMEOUT.total_seconds(), - ) + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + results_sender=results_channel.new_sender(), + component_pool_status_sender=battery_status_channel.new_sender(), + wait_for_data_sec=0.1, + ): + await requests_channel.new_sender().send(request) + result_rx = results_channel.new_receiver() + + done, pending = await asyncio.wait( + [asyncio.create_task(result_rx.receive())], + timeout=SAFETY_TIMEOUT.total_seconds(), + ) assert len(pending) == 0 assert len(done) == 1 + result = done.pop().result() assert isinstance(result, Success) - assert result.succeeded_components == {19} - assert result.excess_power.isclose(Power.from_watts(700.0)) - assert result.succeeded_power.isclose(Power.from_watts(500.0)) + assert result.succeeded_power.isclose(Power.from_kilowatts(1.0)) + assert result.excess_power.isclose(Power.zero(), abs_tol=1e-9) assert result.request == request - await mocks.stop() + async def test_not_all_batteries_are_working(self, mocker: MockerFixture) -> None: + """Test if power distribution works if not all batteries are working.""" + async with _mocks(mocker, grid_meter=False) as mocks: + await self.init_component_data(mocks) + + batteries = {9, 19} + + await self._patch_battery_pool_status(mocks, mocker, batteries - {9}) + + requests_channel = Broadcast[Request]("power_distributor requests") + results_channel = Broadcast[Result]("power_distributor results") + + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + results_sender=results_channel.new_sender(), + component_pool_status_sender=battery_status_channel.new_sender(), + wait_for_data_sec=0.1, + ): + request = Request( + power=Power.from_kilowatts(1.2), + component_ids=batteries, + request_timeout=SAFETY_TIMEOUT, + ) + + await requests_channel.new_sender().send(request) + result_rx = results_channel.new_receiver() + + done, pending = await asyncio.wait( + [asyncio.create_task(result_rx.receive())], + timeout=SAFETY_TIMEOUT.total_seconds(), + ) + + assert len(pending) == 0 + assert len(done) == 1 + result = done.pop().result() + assert isinstance(result, Success) + assert result.succeeded_components == {19} + assert result.excess_power.isclose(Power.from_watts(700.0)) + assert result.succeeded_power.isclose(Power.from_watts(500.0)) + assert result.request == request async def test_partial_failure_result(self, mocker: MockerFixture) -> None: """Test power results when the microgrid failed to set power for one of the batteries.""" - mocks = await Mocks.new(mocker, grid_meter=False) - await self.init_component_data(mocks) - - batteries = {9, 19, 29} - failed_batteries = {9} - failed_power = 500.0 - - await self._patch_battery_pool_status(mocks, mocker, batteries) + async with _mocks(mocker, grid_meter=False) as mocks: + await self.init_component_data(mocks) - mocker.patch( - "frequenz.sdk.actor.power_distributing._component_managers._battery_manager" - ".BatteryManager._parse_result", - return_value=(failed_power, failed_batteries), - ) - - requests_channel = Broadcast[Request]("power_distributor requests") - results_channel = Broadcast[Result]("power_distributor results") - - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") - async with PowerDistributingActor( - requests_receiver=requests_channel.new_receiver(), - results_sender=results_channel.new_sender(), - component_pool_status_sender=battery_status_channel.new_sender(), - wait_for_data_sec=0.1, - ): - request = Request( - power=Power.from_kilowatts(1.70), - component_ids=batteries, - request_timeout=SAFETY_TIMEOUT, - ) + batteries = {9, 19, 29} + failed_batteries = {9} + failed_power = 500.0 - await requests_channel.new_sender().send(request) - result_rx = results_channel.new_receiver() + await self._patch_battery_pool_status(mocks, mocker, batteries) - done, pending = await asyncio.wait( - [asyncio.create_task(result_rx.receive())], - timeout=SAFETY_TIMEOUT.total_seconds(), + mocker.patch( + "frequenz.sdk.actor.power_distributing._component_managers._battery_manager" + ".BatteryManager._parse_result", + return_value=(failed_power, failed_batteries), ) - assert len(pending) == 0 - assert len(done) == 1 - result = done.pop().result() - assert isinstance(result, PartialFailure) - assert result.succeeded_components == batteries - failed_batteries - assert result.failed_components == failed_batteries - assert result.succeeded_power.isclose(Power.from_watts(1000.0)) - assert result.failed_power.isclose(Power.from_watts(failed_power)) - assert result.excess_power.isclose(Power.from_watts(200.0)) - assert result.request == request - await mocks.stop() + requests_channel = Broadcast[Request]("power_distributor requests") + results_channel = Broadcast[Result]("power_distributor results") + + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + async with PowerDistributingActor( + requests_receiver=requests_channel.new_receiver(), + results_sender=results_channel.new_sender(), + component_pool_status_sender=battery_status_channel.new_sender(), + wait_for_data_sec=0.1, + ): + request = Request( + power=Power.from_kilowatts(1.70), + component_ids=batteries, + request_timeout=SAFETY_TIMEOUT, + ) + + await requests_channel.new_sender().send(request) + result_rx = results_channel.new_receiver() + + done, pending = await asyncio.wait( + [asyncio.create_task(result_rx.receive())], + timeout=SAFETY_TIMEOUT.total_seconds(), + ) + assert len(pending) == 0 + assert len(done) == 1 + result = done.pop().result() + assert isinstance(result, PartialFailure) + assert result.succeeded_components == batteries - failed_batteries + assert result.failed_components == failed_batteries + assert result.succeeded_power.isclose(Power.from_watts(1000.0)) + assert result.failed_power.isclose(Power.from_watts(failed_power)) + assert result.excess_power.isclose(Power.from_watts(200.0)) + assert result.request == request diff --git a/tests/actor/test_battery_pool_status.py b/tests/actor/test_battery_pool_status.py index 7bdf9d4fe..edcbe3646 100644 --- a/tests/actor/test_battery_pool_status.py +++ b/tests/actor/test_battery_pool_status.py @@ -32,77 +32,85 @@ async def test_batteries_status(self, mocker: MockerFixture) -> None: Args: mocker: Pytest mocker fixture. """ - mock_microgrid = MockMicrogrid(grid_meter=True) + mock_microgrid = MockMicrogrid(grid_meter=True, mocker=mocker) mock_microgrid.add_batteries(3) - await mock_microgrid.start(mocker) - batteries = { - battery.component_id - for battery in mock_microgrid.mock_client.component_graph.components( - component_categories={ComponentCategory.BATTERY} + async with mock_microgrid: + batteries = { + battery.component_id + for battery in mock_microgrid.mock_client.component_graph.components( + component_categories={ComponentCategory.BATTERY} + ) + } + battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") + battery_status_recv = battery_status_channel.new_receiver(maxsize=1) + batteries_status = ComponentPoolStatusTracker( + component_ids=batteries, + component_status_sender=battery_status_channel.new_sender(), + max_data_age_sec=5, + max_blocking_duration_sec=30, + component_status_tracker_type=BatteryStatusTracker, ) - } - battery_status_channel = Broadcast[ComponentPoolStatus]("battery_status") - battery_status_recv = battery_status_channel.new_receiver(maxsize=1) - batteries_status = ComponentPoolStatusTracker( - component_ids=batteries, - component_status_sender=battery_status_channel.new_sender(), - max_data_age_sec=5, - max_blocking_duration_sec=30, - component_status_tracker_type=BatteryStatusTracker, - ) - await asyncio.sleep(0.1) - - expected_working: set[int] = set() - assert batteries_status.get_working_components(batteries) == expected_working - - batteries_list = list(batteries) - - await mock_microgrid.mock_client.send( - battery_data(component_id=batteries_list[0]) - ) - await asyncio.sleep(0.1) - assert batteries_status.get_working_components(batteries) == expected_working - - expected_working.add(batteries_list[0]) - await mock_microgrid.mock_client.send( - inverter_data(component_id=batteries_list[0] - 1) - ) - await asyncio.sleep(0.1) - assert batteries_status.get_working_components(batteries) == expected_working - msg = await asyncio.wait_for(battery_status_recv.receive(), timeout=0.2) - assert msg == batteries_status._current_status - - await mock_microgrid.mock_client.send( - inverter_data(component_id=batteries_list[1] - 1) - ) - await mock_microgrid.mock_client.send( - battery_data(component_id=batteries_list[1]) - ) - - await mock_microgrid.mock_client.send( - inverter_data(component_id=batteries_list[2] - 1) - ) - await mock_microgrid.mock_client.send( - battery_data(component_id=batteries_list[2]) - ) - - expected_working = set(batteries_list) - await asyncio.sleep(0.1) - assert batteries_status.get_working_components(batteries) == expected_working - msg = await asyncio.wait_for(battery_status_recv.receive(), timeout=0.2) - assert msg == batteries_status._current_status - - await batteries_status.update_status( - succeeded_components={9}, failed_components={19, 29} - ) - await asyncio.sleep(0.1) - assert batteries_status.get_working_components(batteries) == {9} - - await batteries_status.update_status( - succeeded_components={9, 19}, failed_components=set() - ) - await asyncio.sleep(0.1) - assert batteries_status.get_working_components(batteries) == {9, 19} - - await batteries_status.stop() + await asyncio.sleep(0.1) + + expected_working: set[int] = set() + assert ( + batteries_status.get_working_components(batteries) == expected_working + ) + + batteries_list = list(batteries) + + await mock_microgrid.mock_client.send( + battery_data(component_id=batteries_list[0]) + ) + await asyncio.sleep(0.1) + assert ( + batteries_status.get_working_components(batteries) == expected_working + ) + + expected_working.add(batteries_list[0]) + await mock_microgrid.mock_client.send( + inverter_data(component_id=batteries_list[0] - 1) + ) + await asyncio.sleep(0.1) + assert ( + batteries_status.get_working_components(batteries) == expected_working + ) + msg = await asyncio.wait_for(battery_status_recv.receive(), timeout=0.2) + assert msg == batteries_status._current_status + + await mock_microgrid.mock_client.send( + inverter_data(component_id=batteries_list[1] - 1) + ) + await mock_microgrid.mock_client.send( + battery_data(component_id=batteries_list[1]) + ) + + await mock_microgrid.mock_client.send( + inverter_data(component_id=batteries_list[2] - 1) + ) + await mock_microgrid.mock_client.send( + battery_data(component_id=batteries_list[2]) + ) + + expected_working = set(batteries_list) + await asyncio.sleep(0.1) + assert ( + batteries_status.get_working_components(batteries) == expected_working + ) + msg = await asyncio.wait_for(battery_status_recv.receive(), timeout=0.2) + assert msg == batteries_status._current_status + + await batteries_status.update_status( + succeeded_components={9}, failed_components={19, 29} + ) + await asyncio.sleep(0.1) + assert batteries_status.get_working_components(batteries) == {9} + + await batteries_status.update_status( + succeeded_components={9, 19}, failed_components=set() + ) + await asyncio.sleep(0.1) + assert batteries_status.get_working_components(batteries) == {9, 19} + + await batteries_status.stop() diff --git a/tests/actor/test_battery_status.py b/tests/actor/test_battery_status.py index 0a004c0e4..65cea1438 100644 --- a/tests/actor/test_battery_status.py +++ b/tests/actor/test_battery_status.py @@ -7,12 +7,12 @@ import asyncio import math from collections.abc import AsyncIterator, Iterable +from contextlib import asynccontextmanager from dataclasses import dataclass from datetime import datetime, timedelta, timezone -from typing import Generic, TypeVar +from typing import Any, Generic, TypeVar import pytest -import time_machine # pylint: disable=no-name-in-module from frequenz.api.microgrid.battery_pb2 import ComponentState as BatteryState @@ -27,6 +27,7 @@ # pylint: enable=no-name-in-module from frequenz.channels import Broadcast, Receiver from pytest_mock import MockerFixture +from time_machine import TimeMachineFixture from frequenz.sdk.actor.power_distributing._battery_status_tracker import ( BatteryStatusTracker, @@ -145,13 +146,32 @@ async def recv_timeout(recv: Receiver[T], timeout: float = 0.1) -> T | type[_Tim return _Timeout +@asynccontextmanager +async def battery_status_tracker( + *args: Any, **kwargs: Any +) -> AsyncIterator[BatteryStatusTracker]: + """Create BatteryStatusTracker with given arguments. + + Args: + *args: Arguments for BatteryStatusTracker. + **kwargs: Arguments for BatteryStatusTracker. + + Yields: + BatteryStatusTracker with given arguments. + """ + tracker = BatteryStatusTracker(*args, **kwargs) + try: + yield tracker + finally: + await tracker.stop() + + # pylint: disable=protected-access, unused-argument class TestBatteryStatus: """Tests BatteryStatusTracker.""" - @time_machine.travel("2022-01-01 00:00 UTC", tick=False) async def test_sync_update_status_with_messages( - self, mocker: MockerFixture + self, mocker: MockerFixture, time_machine: TimeMachineFixture ) -> None: """Test if messages changes battery status. @@ -160,159 +180,164 @@ async def test_sync_update_status_with_messages( Args: mocker: Pytest mocker fixture. + time_machine: Pytest time_machine fixture. """ - mock_microgrid = MockMicrogrid(grid_meter=True) + mock_microgrid = MockMicrogrid(grid_meter=True, mocker=mocker) mock_microgrid.add_batteries(3) - await mock_microgrid.start(mocker) status_channel = Broadcast[ComponentStatus]("battery_status") set_power_result_channel = Broadcast[SetPowerResult]("set_power_result") - tracker = BatteryStatusTracker( + async with mock_microgrid, battery_status_tracker( BATTERY_ID, max_data_age_sec=5, max_blocking_duration_sec=30, status_sender=status_channel.new_sender(), set_power_result_receiver=set_power_result_channel.new_receiver(), - ) + ) as tracker: + time_machine.move_to("2022-01-01 00:00 UTC", tick=False) + assert tracker.battery_id == BATTERY_ID + assert tracker._last_status == ComponentStatusEnum.NOT_WORKING - assert tracker.battery_id == BATTERY_ID - assert tracker._last_status == ComponentStatusEnum.NOT_WORKING - - tracker._handle_status_inverter(inverter_data(component_id=INVERTER_ID)) - assert tracker._get_new_status_if_changed() is None + tracker._handle_status_inverter(inverter_data(component_id=INVERTER_ID)) + assert tracker._get_new_status_if_changed() is None - tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING + tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) + assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING - # --- Send correct message once again, status should not change --- - tracker._handle_status_inverter(inverter_data(component_id=INVERTER_ID)) - assert tracker._get_new_status_if_changed() is None + # --- Send correct message once again, status should not change --- + tracker._handle_status_inverter(inverter_data(component_id=INVERTER_ID)) + assert tracker._get_new_status_if_changed() is None - tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) - assert tracker._get_new_status_if_changed() is None + tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) + assert tracker._get_new_status_if_changed() is None - # --- Send outdated message --- - tracker._handle_status_inverter( - inverter_data( - component_id=INVERTER_ID, - timestamp=datetime.now(tz=timezone.utc) - timedelta(seconds=31), + # --- Send outdated message --- + tracker._handle_status_inverter( + inverter_data( + component_id=INVERTER_ID, + timestamp=datetime.now(tz=timezone.utc) - timedelta(seconds=31), + ) + ) + assert ( + tracker._get_new_status_if_changed() is ComponentStatusEnum.NOT_WORKING ) - ) - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.NOT_WORKING - # --- BatteryRelayState is invalid. --- - tracker._handle_status_battery( - battery_data( - component_id=BATTERY_ID, - relay_state=BatteryRelayState.RELAY_STATE_OPENED, + # --- BatteryRelayState is invalid. --- + tracker._handle_status_battery( + battery_data( + component_id=BATTERY_ID, + relay_state=BatteryRelayState.RELAY_STATE_OPENED, + ) ) - ) - assert tracker._get_new_status_if_changed() is None + assert tracker._get_new_status_if_changed() is None - # --- Inverter started sending data, but battery relays state are still invalid --- - tracker._handle_status_inverter(inverter_data(component_id=INVERTER_ID)) - assert tracker._get_new_status_if_changed() is None + # --- Inverter started sending data, but battery relays state are still invalid --- + tracker._handle_status_inverter(inverter_data(component_id=INVERTER_ID)) + assert tracker._get_new_status_if_changed() is None - tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING + tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) + assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING - # --- Inverter started sending data, but battery relays state are still invalid --- - tracker._handle_status_inverter( - inverter_data( - component_id=INVERTER_ID, - component_state=InverterState.COMPONENT_STATE_SWITCHING_OFF, + # --- Inverter started sending data, but battery relays state are still invalid --- + tracker._handle_status_inverter( + inverter_data( + component_id=INVERTER_ID, + component_state=InverterState.COMPONENT_STATE_SWITCHING_OFF, + ) + ) + assert ( + tracker._get_new_status_if_changed() is ComponentStatusEnum.NOT_WORKING ) - ) - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.NOT_WORKING - inverter_critical_error = InverterError( - code=InverterErrorCode.ERROR_CODE_UNSPECIFIED, - level=ErrorLevel.ERROR_LEVEL_CRITICAL, - msg="", - ) + inverter_critical_error = InverterError( + code=InverterErrorCode.ERROR_CODE_UNSPECIFIED, + level=ErrorLevel.ERROR_LEVEL_CRITICAL, + msg="", + ) - inverter_warning_error = InverterError( - code=InverterErrorCode.ERROR_CODE_UNSPECIFIED, - level=ErrorLevel.ERROR_LEVEL_WARN, - msg="", - ) + inverter_warning_error = InverterError( + code=InverterErrorCode.ERROR_CODE_UNSPECIFIED, + level=ErrorLevel.ERROR_LEVEL_WARN, + msg="", + ) - tracker._handle_status_inverter( - inverter_data( - component_id=INVERTER_ID, - component_state=InverterState.COMPONENT_STATE_SWITCHING_OFF, - errors=[inverter_critical_error, inverter_warning_error], + tracker._handle_status_inverter( + inverter_data( + component_id=INVERTER_ID, + component_state=InverterState.COMPONENT_STATE_SWITCHING_OFF, + errors=[inverter_critical_error, inverter_warning_error], + ) ) - ) - assert tracker._get_new_status_if_changed() is None + assert tracker._get_new_status_if_changed() is None - tracker._handle_status_inverter( - inverter_data( - component_id=INVERTER_ID, - errors=[inverter_critical_error, inverter_warning_error], + tracker._handle_status_inverter( + inverter_data( + component_id=INVERTER_ID, + errors=[inverter_critical_error, inverter_warning_error], + ) ) - ) - assert tracker._get_new_status_if_changed() is None + assert tracker._get_new_status_if_changed() is None - tracker._handle_status_inverter( - inverter_data(component_id=INVERTER_ID, errors=[inverter_warning_error]) - ) + tracker._handle_status_inverter( + inverter_data(component_id=INVERTER_ID, errors=[inverter_warning_error]) + ) - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING + assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING - battery_critical_error = BatteryError( - code=BatteryErrorCode.ERROR_CODE_UNSPECIFIED, - level=ErrorLevel.ERROR_LEVEL_CRITICAL, - msg="", - ) + battery_critical_error = BatteryError( + code=BatteryErrorCode.ERROR_CODE_UNSPECIFIED, + level=ErrorLevel.ERROR_LEVEL_CRITICAL, + msg="", + ) - battery_warning_error = BatteryError( - code=BatteryErrorCode.ERROR_CODE_UNSPECIFIED, - level=ErrorLevel.ERROR_LEVEL_WARN, - msg="", - ) + battery_warning_error = BatteryError( + code=BatteryErrorCode.ERROR_CODE_UNSPECIFIED, + level=ErrorLevel.ERROR_LEVEL_WARN, + msg="", + ) - tracker._handle_status_battery( - battery_data(component_id=BATTERY_ID, errors=[battery_warning_error]) - ) + tracker._handle_status_battery( + battery_data(component_id=BATTERY_ID, errors=[battery_warning_error]) + ) - assert tracker._get_new_status_if_changed() is None + assert tracker._get_new_status_if_changed() is None - tracker._handle_status_battery( - battery_data( - component_id=BATTERY_ID, - errors=[battery_warning_error, battery_critical_error], + tracker._handle_status_battery( + battery_data( + component_id=BATTERY_ID, + errors=[battery_warning_error, battery_critical_error], + ) ) - ) - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.NOT_WORKING - - tracker._handle_status_battery( - battery_data( - component_id=BATTERY_ID, - component_state=BatteryState.COMPONENT_STATE_ERROR, - errors=[battery_warning_error, battery_critical_error], + assert ( + tracker._get_new_status_if_changed() is ComponentStatusEnum.NOT_WORKING ) - ) - assert tracker._get_new_status_if_changed() is None + tracker._handle_status_battery( + battery_data( + component_id=BATTERY_ID, + component_state=BatteryState.COMPONENT_STATE_ERROR, + errors=[battery_warning_error, battery_critical_error], + ) + ) - # Check if NaN capacity changes the battery status. - tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) + assert tracker._get_new_status_if_changed() is None - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING + # Check if NaN capacity changes the battery status. + tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) - tracker._handle_status_battery( - battery_data(component_id=BATTERY_ID, capacity=math.nan) - ) + assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.NOT_WORKING + tracker._handle_status_battery( + battery_data(component_id=BATTERY_ID, capacity=math.nan) + ) - await tracker.stop() - await mock_microgrid.cleanup() + assert ( + tracker._get_new_status_if_changed() is ComponentStatusEnum.NOT_WORKING + ) async def test_sync_blocking_feature(self, mocker: MockerFixture) -> None: """Test if status changes when SetPowerResult message is received. @@ -323,52 +348,88 @@ async def test_sync_blocking_feature(self, mocker: MockerFixture) -> None: Args: mocker: Pytest mocker fixture. """ - mock_microgrid = MockMicrogrid(grid_meter=True) + import time_machine # pylint: disable=import-outside-toplevel + + mock_microgrid = MockMicrogrid(grid_meter=True, mocker=mocker) mock_microgrid.add_batteries(3) - await mock_microgrid.start(mocker) status_channel = Broadcast[ComponentStatus]("battery_status") set_power_result_channel = Broadcast[SetPowerResult]("set_power_result") - # increase max_data_age_sec for blocking tests. - # Otherwise it will block blocking. - tracker = BatteryStatusTracker( + async with mock_microgrid, battery_status_tracker( + # increase max_data_age_sec for blocking tests. + # Otherwise it will block blocking. BATTERY_ID, max_data_age_sec=500, max_blocking_duration_sec=30, status_sender=status_channel.new_sender(), set_power_result_receiver=set_power_result_channel.new_receiver(), - ) + ) as tracker: + with time_machine.travel("2022-01-01 00:00 UTC", tick=False) as time: + tracker._handle_status_inverter(inverter_data(component_id=INVERTER_ID)) - with time_machine.travel("2022-01-01 00:00 UTC", tick=False) as time: - tracker._handle_status_inverter(inverter_data(component_id=INVERTER_ID)) + assert tracker._get_new_status_if_changed() is None - assert tracker._get_new_status_if_changed() is None + tracker._handle_status_battery( + battery_data( + component_id=BATTERY_ID, + component_state=BatteryState.COMPONENT_STATE_ERROR, + ) + ) - tracker._handle_status_battery( - battery_data( - component_id=BATTERY_ID, - component_state=BatteryState.COMPONENT_STATE_ERROR, + assert tracker._get_new_status_if_changed() is None + + # message is not correct, component should not block. + tracker._handle_status_set_power_result( + SetPowerResult(succeeded={1}, failed={BATTERY_ID}) ) - ) - assert tracker._get_new_status_if_changed() is None + assert tracker._get_new_status_if_changed() is None - # message is not correct, component should not block. - tracker._handle_status_set_power_result( - SetPowerResult(succeeded={1}, failed={BATTERY_ID}) - ) + tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) - assert tracker._get_new_status_if_changed() is None + assert ( + tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING + ) - tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) + expected_blocking_timeout = [1, 2, 4, 8, 16, 30, 30] - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING + for timeout in expected_blocking_timeout: + # message is not correct, component should not block. + tracker._handle_status_set_power_result( + SetPowerResult(succeeded={1}, failed={BATTERY_ID}) + ) - expected_blocking_timeout = [1, 2, 4, 8, 16, 30, 30] + assert ( + tracker._get_new_status_if_changed() + is ComponentStatusEnum.UNCERTAIN + ) - for timeout in expected_blocking_timeout: - # message is not correct, component should not block. + # Battery should be still blocked, nothing should happen + time.shift(timeout - 1) + tracker._handle_status_set_power_result( + SetPowerResult(succeeded={1}, failed={BATTERY_ID}) + ) + + assert tracker._get_new_status_if_changed() is None + + tracker._handle_status_battery( + battery_data(component_id=BATTERY_ID) + ) + + assert tracker._get_new_status_if_changed() is None + + time.shift(1) + tracker._handle_status_battery( + battery_data(component_id=BATTERY_ID) + ) + + assert ( + tracker._get_new_status_if_changed() + is ComponentStatusEnum.WORKING + ) + + # should block for 30 sec tracker._handle_status_set_power_result( SetPowerResult(succeeded={1}, failed={BATTERY_ID}) ) @@ -377,67 +438,46 @@ async def test_sync_blocking_feature(self, mocker: MockerFixture) -> None: tracker._get_new_status_if_changed() is ComponentStatusEnum.UNCERTAIN ) + time.shift(28) - # Battery should be still blocked, nothing should happen - time.shift(timeout - 1) - tracker._handle_status_set_power_result( - SetPowerResult(succeeded={1}, failed={BATTERY_ID}) + tracker._handle_status_battery( + battery_data( + component_id=BATTERY_ID, + component_state=BatteryState.COMPONENT_STATE_ERROR, + ) ) - assert tracker._get_new_status_if_changed() is None - - tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) - - assert tracker._get_new_status_if_changed() is None + assert ( + tracker._get_new_status_if_changed() + is ComponentStatusEnum.NOT_WORKING + ) - time.shift(1) + # Message that changed status to correct should unblock the battery. tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) - assert ( tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING ) - # should block for 30 sec - tracker._handle_status_set_power_result( - SetPowerResult(succeeded={1}, failed={BATTERY_ID}) - ) - - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.UNCERTAIN - time.shift(28) - - tracker._handle_status_battery( - battery_data( - component_id=BATTERY_ID, - component_state=BatteryState.COMPONENT_STATE_ERROR, + # should block for 30 sec + tracker._handle_status_set_power_result( + SetPowerResult(succeeded={1}, failed={BATTERY_ID}) ) - ) - - assert ( - tracker._get_new_status_if_changed() is ComponentStatusEnum.NOT_WORKING - ) - - # Message that changed status to correct should unblock the battery. - tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING - - # should block for 30 sec - tracker._handle_status_set_power_result( - SetPowerResult(succeeded={1}, failed={BATTERY_ID}) - ) - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.UNCERTAIN - time.shift(28) - - # If battery succeed, then it should unblock. - tracker._handle_status_set_power_result( - SetPowerResult(succeeded={BATTERY_ID}, failed={19}) - ) - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING + assert ( + tracker._get_new_status_if_changed() + is ComponentStatusEnum.UNCERTAIN + ) + time.shift(28) - await tracker.stop() - await mock_microgrid.cleanup() + # If battery succeed, then it should unblock. + tracker._handle_status_set_power_result( + SetPowerResult(succeeded={BATTERY_ID}, failed={19}) + ) + assert ( + tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING + ) async def test_sync_blocking_interrupted_with_with_max_data( - self, mocker: MockerFixture + self, mocker: MockerFixture, time_machine: TimeMachineFixture ) -> None: """Test if status changes when SetPowerResult message is received. @@ -446,23 +486,24 @@ async def test_sync_blocking_interrupted_with_with_max_data( Args: mocker: Pytest mocker fixture. + time_machine: Pytest time_machine fixture. """ - mock_microgrid = MockMicrogrid(grid_meter=True) + mock_microgrid = MockMicrogrid(grid_meter=True, mocker=mocker) mock_microgrid.add_batteries(3) - await mock_microgrid.start(mocker) status_channel = Broadcast[ComponentStatus]("battery_status") set_power_result_channel = Broadcast[SetPowerResult]("set_power_result") - tracker = BatteryStatusTracker( + async with mock_microgrid, battery_status_tracker( BATTERY_ID, max_data_age_sec=5, max_blocking_duration_sec=30, status_sender=status_channel.new_sender(), set_power_result_receiver=set_power_result_channel.new_receiver(), - ) + ) as tracker: + start = datetime(2022, 1, 1, tzinfo=timezone.utc) + time_machine.move_to(start, tick=False) - with time_machine.travel("2022-01-01 00:00 UTC", tick=False) as time: tracker._handle_status_inverter(inverter_data(component_id=INVERTER_ID)) assert tracker._get_new_status_if_changed() is None @@ -481,14 +522,10 @@ async def test_sync_blocking_interrupted_with_with_max_data( SetPowerResult(succeeded={1}, failed={BATTERY_ID}) ) assert tracker._get_new_status_if_changed() is None - time.shift(timeout) - - await tracker.stop() - await mock_microgrid.cleanup() + time_machine.move_to(start + timedelta(seconds=timeout)) - @time_machine.travel("2022-01-01 00:00 UTC", tick=False) async def test_sync_blocking_interrupted_with_invalid_message( - self, mocker: MockerFixture + self, mocker: MockerFixture, time_machine: TimeMachineFixture ) -> None: """Test if status changes when SetPowerResult message is received. @@ -497,59 +534,60 @@ async def test_sync_blocking_interrupted_with_invalid_message( Args: mocker: Pytest mocker fixture. + time_machine: Pytest time_machine fixture. """ - mock_microgrid = MockMicrogrid(grid_meter=True) + mock_microgrid = MockMicrogrid(grid_meter=True, mocker=mocker) mock_microgrid.add_batteries(3) - await mock_microgrid.start(mocker) status_channel = Broadcast[ComponentStatus]("battery_status") set_power_result_channel = Broadcast[SetPowerResult]("set_power_result") - tracker = BatteryStatusTracker( + async with mock_microgrid, battery_status_tracker( BATTERY_ID, max_data_age_sec=5, max_blocking_duration_sec=30, status_sender=status_channel.new_sender(), set_power_result_receiver=set_power_result_channel.new_receiver(), - ) + ) as tracker: + time_machine.move_to("2022-01-01 00:00 UTC", tick=False) - tracker._handle_status_inverter(inverter_data(component_id=INVERTER_ID)) - assert tracker._get_new_status_if_changed() is None - - tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING + tracker._handle_status_inverter(inverter_data(component_id=INVERTER_ID)) + assert tracker._get_new_status_if_changed() is None - tracker._handle_status_set_power_result( - SetPowerResult(succeeded={1}, failed={BATTERY_ID}) - ) - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.UNCERTAIN + tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) + assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING - tracker._handle_status_inverter( - inverter_data( - component_id=INVERTER_ID, - component_state=InverterState.COMPONENT_STATE_ERROR, + tracker._handle_status_set_power_result( + SetPowerResult(succeeded={1}, failed={BATTERY_ID}) ) - ) - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.NOT_WORKING + assert tracker._get_new_status_if_changed() is ComponentStatusEnum.UNCERTAIN - tracker._handle_status_set_power_result( - SetPowerResult(succeeded={1}, failed={BATTERY_ID}) - ) - assert tracker._get_new_status_if_changed() is None + tracker._handle_status_inverter( + inverter_data( + component_id=INVERTER_ID, + component_state=InverterState.COMPONENT_STATE_ERROR, + ) + ) + assert ( + tracker._get_new_status_if_changed() is ComponentStatusEnum.NOT_WORKING + ) - tracker._handle_status_set_power_result( - SetPowerResult(succeeded={BATTERY_ID}, failed=set()) - ) - assert tracker._get_new_status_if_changed() is None + tracker._handle_status_set_power_result( + SetPowerResult(succeeded={1}, failed={BATTERY_ID}) + ) + assert tracker._get_new_status_if_changed() is None - tracker._handle_status_inverter(inverter_data(component_id=INVERTER_ID)) - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING + tracker._handle_status_set_power_result( + SetPowerResult(succeeded={BATTERY_ID}, failed=set()) + ) + assert tracker._get_new_status_if_changed() is None - await tracker.stop() - await mock_microgrid.cleanup() + tracker._handle_status_inverter(inverter_data(component_id=INVERTER_ID)) + assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING - @time_machine.travel("2022-01-01 00:00 UTC", tick=False) - async def test_timers(self, mocker: MockerFixture) -> None: + async def test_timers( + self, mocker: MockerFixture, time_machine: TimeMachineFixture + ) -> None: """Test if messages changes battery status. Tests uses FakeSelect to test status in sync way. @@ -557,72 +595,73 @@ async def test_timers(self, mocker: MockerFixture) -> None: Args: mocker: Pytest mocker fixture. + time_machine: Pytest time_machine fixture. """ - mock_microgrid = MockMicrogrid(grid_meter=True) + mock_microgrid = MockMicrogrid(grid_meter=True, mocker=mocker) mock_microgrid.add_batteries(3) - await mock_microgrid.start(mocker) status_channel = Broadcast[ComponentStatus]("battery_status") set_power_result_channel = Broadcast[SetPowerResult]("set_power_result") - tracker = BatteryStatusTracker( + async with mock_microgrid, battery_status_tracker( BATTERY_ID, max_data_age_sec=5, max_blocking_duration_sec=30, status_sender=status_channel.new_sender(), set_power_result_receiver=set_power_result_channel.new_receiver(), - ) + ) as tracker: + time_machine.move_to("2022-01-01 00:00 UTC", tick=False) - battery_timer_spy = mocker.spy(tracker._battery.data_recv_timer, "reset") - inverter_timer_spy = mocker.spy(tracker._inverter.data_recv_timer, "reset") + battery_timer_spy = mocker.spy(tracker._battery.data_recv_timer, "reset") + inverter_timer_spy = mocker.spy(tracker._inverter.data_recv_timer, "reset") - assert tracker.battery_id == BATTERY_ID - assert tracker._last_status == ComponentStatusEnum.NOT_WORKING + assert tracker.battery_id == BATTERY_ID + assert tracker._last_status == ComponentStatusEnum.NOT_WORKING - tracker._handle_status_inverter(inverter_data(component_id=INVERTER_ID)) - assert tracker._get_new_status_if_changed() is None + tracker._handle_status_inverter(inverter_data(component_id=INVERTER_ID)) + assert tracker._get_new_status_if_changed() is None - tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING + tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) + assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING - assert battery_timer_spy.call_count == 1 + assert battery_timer_spy.call_count == 1 - tracker._handle_status_battery_timer() - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.NOT_WORKING + tracker._handle_status_battery_timer() + assert ( + tracker._get_new_status_if_changed() is ComponentStatusEnum.NOT_WORKING + ) - assert battery_timer_spy.call_count == 1 + assert battery_timer_spy.call_count == 1 - tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING + tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) + assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING - assert battery_timer_spy.call_count == 2 + assert battery_timer_spy.call_count == 2 - tracker._handle_status_inverter_timer() - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.NOT_WORKING + tracker._handle_status_inverter_timer() + assert ( + tracker._get_new_status_if_changed() is ComponentStatusEnum.NOT_WORKING + ) - tracker._handle_status_battery_timer() - assert tracker._get_new_status_if_changed() is None + tracker._handle_status_battery_timer() + assert tracker._get_new_status_if_changed() is None - tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) - assert tracker._get_new_status_if_changed() is None + tracker._handle_status_battery(battery_data(component_id=BATTERY_ID)) + assert tracker._get_new_status_if_changed() is None - tracker._handle_status_inverter(inverter_data(component_id=INVERTER_ID)) - assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING + tracker._handle_status_inverter(inverter_data(component_id=INVERTER_ID)) + assert tracker._get_new_status_if_changed() is ComponentStatusEnum.WORKING - assert inverter_timer_spy.call_count == 2 - await tracker.stop() - await mock_microgrid.cleanup() + assert inverter_timer_spy.call_count == 2 - @time_machine.travel("2022-01-01 00:00 UTC", tick=False) async def test_async_battery_status(self, mocker: MockerFixture) -> None: """Test if status changes. Args: mocker: Pytest mocker fixture. """ - mock_microgrid = MockMicrogrid(grid_meter=True) + mock_microgrid = MockMicrogrid(grid_meter=True, mocker=mocker) mock_microgrid.add_batteries(3) - await mock_microgrid.start(mocker) status_channel = Broadcast[ComponentStatus]("battery_status") set_power_result_channel = Broadcast[SetPowerResult]("set_power_result") @@ -630,58 +669,62 @@ async def test_async_battery_status(self, mocker: MockerFixture) -> None: status_receiver = status_channel.new_receiver() set_power_result_sender = set_power_result_channel.new_sender() - tracker = BatteryStatusTracker( + async with mock_microgrid, battery_status_tracker( BATTERY_ID, max_data_age_sec=5, max_blocking_duration_sec=30, status_sender=status_channel.new_sender(), set_power_result_receiver=set_power_result_channel.new_receiver(), - ) - await asyncio.sleep(0.01) + ): + import time_machine # pylint: disable=import-outside-toplevel - with time_machine.travel("2022-01-01 00:00 UTC", tick=False) as time: - await mock_microgrid.mock_client.send( - inverter_data(component_id=INVERTER_ID) - ) - await mock_microgrid.mock_client.send(battery_data(component_id=BATTERY_ID)) - status = await asyncio.wait_for(status_receiver.receive(), timeout=0.1) - assert status.value is ComponentStatusEnum.WORKING + await asyncio.sleep(0.01) - await set_power_result_sender.send( - SetPowerResult(succeeded=set(), failed={BATTERY_ID}) - ) - status = await asyncio.wait_for(status_receiver.receive(), timeout=0.1) - assert status.value is ComponentStatusEnum.UNCERTAIN + with time_machine.travel("2022-01-01 00:00 UTC", tick=False) as time: + await mock_microgrid.mock_client.send( + inverter_data(component_id=INVERTER_ID) + ) + await mock_microgrid.mock_client.send( + battery_data(component_id=BATTERY_ID) + ) + status = await asyncio.wait_for(status_receiver.receive(), timeout=0.1) + assert status.value is ComponentStatusEnum.WORKING - time.shift(2) + await set_power_result_sender.send( + SetPowerResult(succeeded=set(), failed={BATTERY_ID}) + ) + status = await asyncio.wait_for(status_receiver.receive(), timeout=0.1) + assert status.value is ComponentStatusEnum.UNCERTAIN - await mock_microgrid.mock_client.send(battery_data(component_id=BATTERY_ID)) - status = await asyncio.wait_for(status_receiver.receive(), timeout=0.1) - assert status.value is ComponentStatusEnum.WORKING + time.shift(2) - await mock_microgrid.mock_client.send( - inverter_data( - component_id=INVERTER_ID, - timestamp=datetime.now(tz=timezone.utc) - timedelta(seconds=7), + await mock_microgrid.mock_client.send( + battery_data(component_id=BATTERY_ID) ) - ) - status = await asyncio.wait_for(status_receiver.receive(), timeout=0.1) - assert status.value is ComponentStatusEnum.NOT_WORKING - - await set_power_result_sender.send( - SetPowerResult(succeeded=set(), failed={BATTERY_ID}) - ) - await asyncio.sleep(0.3) - assert len(status_receiver) == 0 + status = await asyncio.wait_for(status_receiver.receive(), timeout=0.1) + assert status.value is ComponentStatusEnum.WORKING + + await mock_microgrid.mock_client.send( + inverter_data( + component_id=INVERTER_ID, + timestamp=datetime.now(tz=timezone.utc) - timedelta(seconds=7), + ) + ) + status = await asyncio.wait_for(status_receiver.receive(), timeout=0.1) + assert status.value is ComponentStatusEnum.NOT_WORKING - await mock_microgrid.mock_client.send( - inverter_data(component_id=INVERTER_ID) - ) - status = await asyncio.wait_for(status_receiver.receive(), timeout=0.1) - assert status.value is ComponentStatusEnum.WORKING + await set_power_result_sender.send( + SetPowerResult(succeeded=set(), failed={BATTERY_ID}) + ) + time.shift(10) + await asyncio.sleep(0.3) + assert len(status_receiver) == 0 - await tracker.stop() - await mock_microgrid.cleanup() + await mock_microgrid.mock_client.send( + inverter_data(component_id=INVERTER_ID) + ) + status = await asyncio.wait_for(status_receiver.receive(), timeout=0.1) + assert status.value is ComponentStatusEnum.WORKING class TestBatteryStatusRecovery: @@ -698,32 +741,27 @@ class TestBatteryStatusRecovery: @pytest.fixture async def setup_tracker( - self, mocker: MockerFixture + self, + mocker: MockerFixture, ) -> AsyncIterator[tuple[MockMicrogrid, Receiver[ComponentStatus]]]: """Set a BatteryStatusTracker instance up to run tests with.""" - mock_microgrid = MockMicrogrid(grid_meter=True) + mock_microgrid = MockMicrogrid(grid_meter=True, mocker=mocker) mock_microgrid.add_batteries(1) - await mock_microgrid.start(mocker) status_channel = Broadcast[ComponentStatus]("battery_status") set_power_result_channel = Broadcast[SetPowerResult]("set_power_result") status_receiver = status_channel.new_receiver() - _tracker = BatteryStatusTracker( + async with mock_microgrid, battery_status_tracker( BATTERY_ID, max_data_age_sec=0.1, max_blocking_duration_sec=1, status_sender=status_channel.new_sender(), set_power_result_receiver=set_power_result_channel.new_receiver(), - ) - - await asyncio.sleep(0.05) - - yield (mock_microgrid, status_receiver) - - await _tracker.stop() - await mock_microgrid.cleanup() + ): + await asyncio.sleep(0.05) + yield (mock_microgrid, status_receiver) async def _send_healthy_battery( self, mock_microgrid: MockMicrogrid, timestamp: datetime | None = None diff --git a/tests/microgrid/test_grid.py b/tests/microgrid/test_grid.py index eb5533fec..ffb7cba96 100644 --- a/tests/microgrid/test_grid.py +++ b/tests/microgrid/test_grid.py @@ -3,6 +3,8 @@ """Tests for the `Grid` module.""" +from contextlib import AsyncExitStack + from pytest_mock import MockerFixture import frequenz.sdk.microgrid.component_graph as gr @@ -34,17 +36,19 @@ async def test_grid_1(mocker: MockerFixture) -> None: connections = { Connection(1, 2), } - # pylint: disable=protected-access - graph = gr._MicrogridComponentGraph(components=components, connections=connections) - mockgrid = MockMicrogrid(graph=graph) - await mockgrid.start(mocker) - grid = microgrid.grid() + graph = gr._MicrogridComponentGraph( # pylint: disable=protected-access + components=components, connections=connections + ) + + async with MockMicrogrid(graph=graph, mocker=mocker), AsyncExitStack() as stack: + grid = microgrid.grid() + assert grid is not None + stack.push_async_callback(grid.stop) - assert grid - assert grid.fuse - assert grid.fuse.max_current == Current.from_amperes(0.0) - await mockgrid.cleanup() + assert grid + assert grid.fuse + assert grid.fuse.max_current == Current.from_amperes(0.0) def _create_fuse() -> Fuse: @@ -68,19 +72,19 @@ async def test_grid_2(mocker: MockerFixture) -> None: Connection(1, 2), } - # pylint: disable=protected-access - graph = gr._MicrogridComponentGraph(components=components, connections=connections) - - mockgrid = MockMicrogrid(graph=graph) - await mockgrid.start(mocker) + graph = gr._MicrogridComponentGraph( # pylint: disable=protected-access + components=components, connections=connections + ) - grid = microgrid.grid() - assert grid is not None + async with MockMicrogrid(graph=graph, mocker=mocker), AsyncExitStack() as stack: + grid = microgrid.grid() + assert grid is not None + stack.push_async_callback(grid.stop) - expected_fuse_current = Current.from_amperes(123.0) - expected_fuse = Fuse(expected_fuse_current) + expected_fuse_current = Current.from_amperes(123.0) + expected_fuse = Fuse(expected_fuse_current) - assert grid.fuse == expected_fuse + assert grid.fuse == expected_fuse async def test_grid_3(mocker: MockerFixture) -> None: @@ -93,104 +97,106 @@ async def test_grid_3(mocker: MockerFixture) -> None: Connection(1, 2), } - # pylint: disable=protected-access - graph = gr._MicrogridComponentGraph(components=components, connections=connections) - - mockgrid = MockMicrogrid(graph=graph) - await mockgrid.start(mocker) + graph = gr._MicrogridComponentGraph( # pylint: disable=protected-access + components=components, connections=connections + ) - grid = microgrid.grid() - assert grid is not None - assert grid.fuse is None + async with MockMicrogrid(graph=graph, mocker=mocker), AsyncExitStack() as stack: + grid = microgrid.grid() + assert grid is not None + stack.push_async_callback(grid.stop) + assert grid.fuse is None async def test_grid_power_1(mocker: MockerFixture) -> None: """Test the grid power formula with a grid side meter.""" - mockgrid = MockMicrogrid(grid_meter=True) + mockgrid = MockMicrogrid(grid_meter=True, mocker=mocker) mockgrid.add_batteries(2) mockgrid.add_solar_inverters(1) - await mockgrid.start(mocker) - grid = microgrid.grid() - assert grid, "Grid is not initialized" - - grid_power_recv = grid.power.new_receiver() - - grid_meter_recv = get_resampled_stream( - grid._formula_pool._namespace, # pylint: disable=protected-access - mockgrid.meter_ids[0], - ComponentMetricId.ACTIVE_POWER, - Power.from_watts, - ) results = [] grid_meter_data = [] - for count in range(10): - await mockgrid.mock_resampler.send_meter_power( - [20.0 + count, 12.0, -13.0, -5.0] + async with mockgrid, AsyncExitStack() as stack: + grid = microgrid.grid() + assert grid, "Grid is not initialized" + stack.push_async_callback(grid.stop) + + grid_power_recv = grid.power.new_receiver() + + grid_meter_recv = get_resampled_stream( + grid._formula_pool._namespace, # pylint: disable=protected-access + mockgrid.meter_ids[0], + ComponentMetricId.ACTIVE_POWER, + Power.from_watts, ) - val = await grid_meter_recv.receive() - assert val is not None and val.value is not None and val.value.as_watts() != 0.0 - 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) + for count in range(10): + await mockgrid.mock_resampler.send_meter_power( + [20.0 + count, 12.0, -13.0, -5.0] + ) + val = await grid_meter_recv.receive() + assert ( + val is not None + and val.value is not None + and val.value.as_watts() != 0.0 + ) + grid_meter_data.append(val.value) - await mockgrid.cleanup() - await grid.stop() + val = await grid_power_recv.receive() + assert val is not None and val.value is not None + results.append(val.value) assert equal_float_lists(results, grid_meter_data) async def test_grid_power_2(mocker: MockerFixture) -> None: """Test the grid power formula without a grid side meter.""" - mockgrid = MockMicrogrid(grid_meter=False) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_consumer_meters(1) mockgrid.add_batteries(1, no_meter=False) mockgrid.add_batteries(1, no_meter=True) mockgrid.add_solar_inverters(1) - await mockgrid.start(mocker) - grid = microgrid.grid() - assert grid, "Grid is not initialized" - - grid_power_recv = grid.power.new_receiver() - - component_receivers = [ - get_resampled_stream( - grid._formula_pool._namespace, # pylint: disable=protected-access - component_id, - ComponentMetricId.ACTIVE_POWER, - Power.from_watts, - ) - for component_id in [ - *mockgrid.meter_ids, - # The last battery has no meter, so we get the power from the inverter - mockgrid.battery_inverter_ids[-1], - ] - ] results: list[Quantity] = [] meter_sums: list[Quantity] = [] - for count in range(10): - await mockgrid.mock_resampler.send_meter_power([20.0 + count, 12.0, -13.0]) - await mockgrid.mock_resampler.send_bat_inverter_power([0.0, -5.0]) - meter_sum = 0.0 - for recv in component_receivers: - val = await recv.receive() - assert ( - val is not None - and val.value is not None - and val.value.as_watts() != 0.0 + async with mockgrid, AsyncExitStack() as stack: + grid = microgrid.grid() + assert grid, "Grid is not initialized" + stack.push_async_callback(grid.stop) + + grid_power_recv = grid.power.new_receiver() + + component_receivers = [ + get_resampled_stream( + grid._formula_pool._namespace, # pylint: disable=protected-access + component_id, + ComponentMetricId.ACTIVE_POWER, + Power.from_watts, ) - meter_sum += val.value.as_watts() - - val = await grid_power_recv.receive() - assert val is not None and val.value is not None - results.append(val.value) - meter_sums.append(Quantity(meter_sum)) + for component_id in [ + *mockgrid.meter_ids, + # The last battery has no meter, so we get the power from the inverter + mockgrid.battery_inverter_ids[-1], + ] + ] - await mockgrid.cleanup() - await grid.stop() + for count in range(10): + await mockgrid.mock_resampler.send_meter_power([20.0 + count, 12.0, -13.0]) + await mockgrid.mock_resampler.send_bat_inverter_power([0.0, -5.0]) + meter_sum = 0.0 + for recv in component_receivers: + val = await recv.receive() + assert ( + val is not None + and val.value is not None + and val.value.as_watts() != 0.0 + ) + meter_sum += val.value.as_watts() + + val = await grid_power_recv.receive() + assert val is not None and val.value is not None + results.append(val.value) + meter_sums.append(Quantity(meter_sum)) assert len(results) == 10 assert equal_float_lists(results, meter_sums) @@ -200,62 +206,59 @@ async def test_grid_production_consumption_power_consumer_meter( mocker: MockerFixture, ) -> None: """Test the grid production and consumption power formulas.""" - mockgrid = MockMicrogrid(grid_meter=False) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_consumer_meters() mockgrid.add_batteries(2) mockgrid.add_solar_inverters(1) - await mockgrid.start(mocker) - grid = microgrid.grid() - assert grid, "Grid is not initialized" - grid_recv = grid.power.new_receiver() + async with mockgrid, AsyncExitStack() as stack: + grid = microgrid.grid() + assert grid, "Grid is not initialized" + stack.push_async_callback(grid.stop) - await mockgrid.mock_resampler.send_meter_power([1.0, 2.0, 3.0, 4.0]) - assert (await grid_recv.receive()).value == Power.from_watts(10.0) + grid_recv = grid.power.new_receiver() - await mockgrid.mock_resampler.send_meter_power([1.0, 2.0, -3.0, -4.0]) - assert (await grid_recv.receive()).value == Power.from_watts(-4.0) + await mockgrid.mock_resampler.send_meter_power([1.0, 2.0, 3.0, 4.0]) + assert (await grid_recv.receive()).value == Power.from_watts(10.0) - await mockgrid.cleanup() - await grid.stop() + await mockgrid.mock_resampler.send_meter_power([1.0, 2.0, -3.0, -4.0]) + assert (await grid_recv.receive()).value == Power.from_watts(-4.0) async def test_grid_production_consumption_power_no_grid_meter( mocker: MockerFixture, ) -> None: """Test the grid production and consumption power formulas.""" - mockgrid = MockMicrogrid(grid_meter=False) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_batteries(2) mockgrid.add_solar_inverters(1) - await mockgrid.start(mocker) - grid = microgrid.grid() - assert grid, "Grid is not initialized" - grid_recv = grid.power.new_receiver() + async with mockgrid, AsyncExitStack() as stack: + grid = microgrid.grid() + assert grid, "Grid is not initialized" + stack.push_async_callback(grid.stop) - await mockgrid.mock_resampler.send_meter_power([2.5, 3.5, 4.0]) - assert (await grid_recv.receive()).value == Power.from_watts(10.0) + grid_recv = grid.power.new_receiver() - await mockgrid.mock_resampler.send_meter_power([3.0, -3.0, -4.0]) - assert (await grid_recv.receive()).value == Power.from_watts(-4.0) + await mockgrid.mock_resampler.send_meter_power([2.5, 3.5, 4.0]) + assert (await grid_recv.receive()).value == Power.from_watts(10.0) - await mockgrid.cleanup() - await grid.stop() + await mockgrid.mock_resampler.send_meter_power([3.0, -3.0, -4.0]) + assert (await grid_recv.receive()).value == Power.from_watts(-4.0) async def test_consumer_power_2_grid_meters(mocker: MockerFixture) -> None: """Test the grid power formula with two grid meters.""" - mockgrid = MockMicrogrid(grid_meter=False) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) # with no further successor these will be detected as grid meters mockgrid.add_consumer_meters(2) - await mockgrid.start(mocker) - grid = microgrid.grid() - assert grid, "Grid is not initialized" - grid_recv = grid.power.new_receiver() + async with mockgrid, AsyncExitStack() as stack: + grid = microgrid.grid() + assert grid, "Grid is not initialized" + stack.push_async_callback(grid.stop) - await mockgrid.mock_resampler.send_meter_power([1.0, 2.0]) - assert (await grid_recv.receive()).value == Power.from_watts(3.0) + grid_recv = grid.power.new_receiver() - await mockgrid.cleanup() - await grid.stop() + await mockgrid.mock_resampler.send_meter_power([1.0, 2.0]) + assert (await grid_recv.receive()).value == Power.from_watts(3.0) diff --git a/tests/timeseries/_battery_pool/test_battery_pool.py b/tests/timeseries/_battery_pool/test_battery_pool.py index 4f2416cbd..625223a93 100644 --- a/tests/timeseries/_battery_pool/test_battery_pool.py +++ b/tests/timeseries/_battery_pool/test_battery_pool.py @@ -487,23 +487,21 @@ 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_meter=True) + mockgrid = MockMicrogrid(grid_meter=True, mocker=mocker) mockgrid.add_batteries(2) - await mockgrid.start(mocker) - battery_pool = microgrid.battery_pool() - power_receiver = battery_pool.power.new_receiver() - - await mockgrid.mock_resampler.send_bat_inverter_power([2.0, 3.0]) - assert (await power_receiver.receive()).value == Power.from_watts(5.0) + async with mockgrid: + battery_pool = microgrid.battery_pool() + power_receiver = battery_pool.power.new_receiver() - await mockgrid.mock_resampler.send_bat_inverter_power([-2.0, -5.0]) - assert (await power_receiver.receive()).value == Power.from_watts(-7.0) + await mockgrid.mock_resampler.send_bat_inverter_power([2.0, 3.0]) + assert (await power_receiver.receive()).value == Power.from_watts(5.0) - await mockgrid.mock_resampler.send_bat_inverter_power([2.0, -5.0]) - assert (await power_receiver.receive()).value == Power.from_watts(-3.0) + await mockgrid.mock_resampler.send_bat_inverter_power([-2.0, -5.0]) + assert (await power_receiver.receive()).value == Power.from_watts(-7.0) - await mockgrid.cleanup() + await mockgrid.mock_resampler.send_bat_inverter_power([2.0, -5.0]) + assert (await power_receiver.receive()).value == Power.from_watts(-3.0) async def run_capacity_test( # pylint: disable=too-many-locals diff --git a/tests/timeseries/_formula_engine/test_formula_composition.py b/tests/timeseries/_formula_engine/test_formula_composition.py index 2b6af120a..8775c931d 100644 --- a/tests/timeseries/_formula_engine/test_formula_composition.py +++ b/tests/timeseries/_formula_engine/test_formula_composition.py @@ -5,6 +5,7 @@ import math +from contextlib import AsyncExitStack import pytest from pytest_mock import MockerFixture @@ -25,386 +26,440 @@ async def test_formula_composition( # pylint: disable=too-many-locals mocker: MockerFixture, ) -> None: """Test the composition of formulas.""" - mockgrid = MockMicrogrid(grid_meter=False, num_namespaces=2) + mockgrid = MockMicrogrid(grid_meter=False, num_namespaces=2, mocker=mocker) 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() - grid = microgrid.grid() - grid_meter_recv = get_resampled_stream( - grid._formula_pool._namespace, # pylint: disable=protected-access - mockgrid.meter_ids[0], - ComponentMetricId.ACTIVE_POWER, - Power.from_watts, - ) - grid_power_recv = grid.power.new_receiver() - battery_power_recv = battery_pool.power.new_receiver() - pv_power_recv = logical_meter.pv_power.new_receiver() - engine = (logical_meter.pv_power + battery_pool.power).build("inv_power") - inv_calc_recv = engine.new_receiver() + async with mockgrid, AsyncExitStack() as stack: + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) - await mockgrid.mock_resampler.send_bat_inverter_power([10.0, 12.0, 14.0]) - await mockgrid.mock_resampler.send_meter_power( - [100.0, 10.0, 12.0, 14.0, -100.0, -200.0] - ) + battery_pool = microgrid.battery_pool() + stack.push_async_callback( + battery_pool._battery_pool.stop # pylint: disable=protected-access + ) - grid_pow = await grid_power_recv.receive() - pv_pow = await pv_power_recv.receive() - bat_pow = await battery_power_recv.receive() - main_pow = await grid_meter_recv.receive() - inv_calc_pow = await inv_calc_recv.receive() - - assert ( - grid_pow is not None - and grid_pow.value is not None - and math.isclose(grid_pow.value.base_value, -164.0) - ) # 100 + 10 + 12 + 14 + -100 + -200 - assert ( - bat_pow is not None - and bat_pow.value is not None - and math.isclose(bat_pow.value.base_value, 36.0) - ) # 10 + 12 + 14 - assert ( - pv_pow is not None - and pv_pow.value is not None - and math.isclose(pv_pow.value.base_value, -300.0) - ) # -100 + -200 - assert ( - inv_calc_pow is not None - and inv_calc_pow.value is not None - and math.isclose(inv_calc_pow.value.base_value, -264.0) # -300 + 36 - ) - assert ( - main_pow is not None - and main_pow.value is not None - and math.isclose(main_pow.value.base_value, 100.0) - ) + grid = microgrid.grid() + stack.push_async_callback(grid.stop) - assert math.isclose( - inv_calc_pow.value.base_value, - pv_pow.value.base_value + bat_pow.value.base_value, - ) - assert math.isclose( - grid_pow.value.base_value, - inv_calc_pow.value.base_value + main_pow.value.base_value, - ) + grid_meter_recv = get_resampled_stream( + grid._formula_pool._namespace, # pylint: disable=protected-access + mockgrid.meter_ids[0], + ComponentMetricId.ACTIVE_POWER, + Power.from_watts, + ) + grid_power_recv = grid.power.new_receiver() + battery_power_recv = battery_pool.power.new_receiver() + pv_power_recv = logical_meter.pv_power.new_receiver() + + engine = (logical_meter.pv_power + battery_pool.power).build("inv_power") + stack.push_async_callback(engine._stop) # pylint: disable=protected-access + + inv_calc_recv = engine.new_receiver() + + await mockgrid.mock_resampler.send_bat_inverter_power([10.0, 12.0, 14.0]) + await mockgrid.mock_resampler.send_meter_power( + [100.0, 10.0, 12.0, 14.0, -100.0, -200.0] + ) + + grid_pow = await grid_power_recv.receive() + pv_pow = await pv_power_recv.receive() + bat_pow = await battery_power_recv.receive() + main_pow = await grid_meter_recv.receive() + inv_calc_pow = await inv_calc_recv.receive() - await mockgrid.cleanup() - await engine._stop() # pylint: disable=protected-access - await battery_pool._battery_pool.stop() # pylint: disable=protected-access - await logical_meter.stop() - await grid.stop() + assert ( + grid_pow is not None + and grid_pow.value is not None + and math.isclose(grid_pow.value.base_value, -164.0) + ) # 100 + 10 + 12 + 14 + -100 + -200 + assert ( + bat_pow is not None + and bat_pow.value is not None + and math.isclose(bat_pow.value.base_value, 36.0) + ) # 10 + 12 + 14 + assert ( + pv_pow is not None + and pv_pow.value is not None + and math.isclose(pv_pow.value.base_value, -300.0) + ) # -100 + -200 + assert ( + inv_calc_pow is not None + and inv_calc_pow.value is not None + and math.isclose(inv_calc_pow.value.base_value, -264.0) # -300 + 36 + ) + assert ( + main_pow is not None + and main_pow.value is not None + and math.isclose(main_pow.value.base_value, 100.0) + ) + + assert math.isclose( + inv_calc_pow.value.base_value, + pv_pow.value.base_value + bat_pow.value.base_value, + ) + assert math.isclose( + grid_pow.value.base_value, + inv_calc_pow.value.base_value + main_pow.value.base_value, + ) async def test_formula_composition_missing_pv(self, mocker: MockerFixture) -> None: """Test the composition of formulas with missing PV power data.""" - mockgrid = MockMicrogrid(grid_meter=False) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_batteries(3) - await mockgrid.start(mocker) - battery_pool = microgrid.battery_pool() - logical_meter = microgrid.logical_meter() - - battery_power_recv = battery_pool.power.new_receiver() - pv_power_recv = logical_meter.pv_power.new_receiver() - engine = (logical_meter.pv_power + battery_pool.power).build("inv_power") - inv_calc_recv = engine.new_receiver() count = 0 - for _ in range(10): - await mockgrid.mock_resampler.send_bat_inverter_power( - [10.0 + count, 12.0 + count, 14.0 + count] + async with mockgrid, AsyncExitStack() as stack: + battery_pool = microgrid.battery_pool() + stack.push_async_callback( + battery_pool._battery_pool.stop # pylint: disable=protected-access ) - await mockgrid.mock_resampler.send_non_existing_component_value() - bat_pow = await battery_power_recv.receive() - pv_pow = await pv_power_recv.receive() - inv_pow = await inv_calc_recv.receive() + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) - assert inv_pow == bat_pow - assert ( - pv_pow.timestamp == inv_pow.timestamp - and pv_pow.value == Power.from_watts(0.0) - ) - count += 1 + battery_power_recv = battery_pool.power.new_receiver() + pv_power_recv = logical_meter.pv_power.new_receiver() + engine = (logical_meter.pv_power + battery_pool.power).build("inv_power") + stack.push_async_callback(engine._stop) # pylint: disable=protected-access + + inv_calc_recv = engine.new_receiver() + + for _ in range(10): + await mockgrid.mock_resampler.send_bat_inverter_power( + [10.0 + count, 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() + inv_pow = await inv_calc_recv.receive() - await mockgrid.cleanup() - await engine._stop() # pylint: disable=protected-access - await battery_pool._battery_pool.stop() # pylint: disable=protected-access - await logical_meter.stop() + assert inv_pow == bat_pow + assert ( + pv_pow.timestamp == inv_pow.timestamp + and pv_pow.value == Power.from_watts(0.0) + ) + count += 1 assert count == 10 async def test_formula_composition_missing_bat(self, mocker: MockerFixture) -> None: """Test the composition of formulas with missing battery power data.""" - mockgrid = MockMicrogrid(grid_meter=False) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_solar_inverters(2) - await mockgrid.start(mocker) - battery_pool = microgrid.battery_pool() - logical_meter = microgrid.logical_meter() - - battery_power_recv = battery_pool.power.new_receiver() - pv_power_recv = logical_meter.pv_power.new_receiver() - engine = (logical_meter.pv_power + battery_pool.power).build("inv_power") - inv_calc_recv = engine.new_receiver() count = 0 - for _ in range(10): - 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() - inv_pow = await inv_calc_recv.receive() - - assert inv_pow == pv_pow - assert ( - bat_pow.timestamp == inv_pow.timestamp - and bat_pow.value == Power.from_watts(0.0) + async with mockgrid, AsyncExitStack() as stack: + battery_pool = microgrid.battery_pool() + stack.push_async_callback( + battery_pool._battery_pool.stop # pylint: disable=protected-access ) - count += 1 - - await mockgrid.cleanup() - await engine._stop() # pylint: disable=protected-access - await battery_pool._battery_pool.stop() # pylint: disable=protected-access - await logical_meter.stop() + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) + + battery_power_recv = battery_pool.power.new_receiver() + pv_power_recv = logical_meter.pv_power.new_receiver() + engine = (logical_meter.pv_power + battery_pool.power).build("inv_power") + stack.push_async_callback(engine._stop) # pylint: disable=protected-access + + inv_calc_recv = engine.new_receiver() + + for _ in range(10): + 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() + inv_pow = await inv_calc_recv.receive() + + assert inv_pow == pv_pow + assert ( + bat_pow.timestamp == inv_pow.timestamp + and bat_pow.value == Power.from_watts(0.0) + ) + count += 1 assert count == 10 async def test_formula_composition_min_max(self, mocker: MockerFixture) -> None: """Test the composition of formulas with the min and max.""" - mockgrid = MockMicrogrid(grid_meter=True) + mockgrid = MockMicrogrid(grid_meter=True, mocker=mocker) mockgrid.add_chps(1) - await mockgrid.start(mocker) - logical_meter = microgrid.logical_meter() - grid = microgrid.grid() - engine_min = grid.power.min(logical_meter.chp_power).build("grid_power_min") - engine_min_rx = engine_min.new_receiver() - engine_max = grid.power.max(logical_meter.chp_power).build("grid_power_max") - engine_max_rx = engine_max.new_receiver() + async with mockgrid, AsyncExitStack() as stack: + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) - await mockgrid.mock_resampler.send_meter_power([100.0, 200.0]) + grid = microgrid.grid() + stack.push_async_callback(grid.stop) - # Test min - min_pow = await engine_min_rx.receive() - assert ( - min_pow and min_pow.value and min_pow.value.isclose(Power.from_watts(100.0)) - ) + engine_min = grid.power.min(logical_meter.chp_power).build("grid_power_min") + stack.push_async_callback( + engine_min._stop # pylint: disable=protected-access + ) + engine_min_rx = engine_min.new_receiver() - # Test max - max_pow = await engine_max_rx.receive() - assert ( - max_pow and max_pow.value and max_pow.value.isclose(Power.from_watts(200.0)) - ) + engine_max = grid.power.max(logical_meter.chp_power).build("grid_power_max") + stack.push_async_callback( + engine_max._stop # pylint: disable=protected-access + ) + engine_max_rx = engine_max.new_receiver() - await mockgrid.mock_resampler.send_meter_power([-100.0, -200.0]) + await mockgrid.mock_resampler.send_meter_power([100.0, 200.0]) - # Test min - min_pow = await engine_min_rx.receive() - assert ( - min_pow - and min_pow.value - and min_pow.value.isclose(Power.from_watts(-200.0)) - ) + # Test min + min_pow = await engine_min_rx.receive() + assert ( + min_pow + and min_pow.value + and min_pow.value.isclose(Power.from_watts(100.0)) + ) - # Test max - max_pow = await engine_max_rx.receive() - assert ( - max_pow - and max_pow.value - and max_pow.value.isclose(Power.from_watts(-100.0)) - ) + # Test max + max_pow = await engine_max_rx.receive() + assert ( + max_pow + and max_pow.value + and max_pow.value.isclose(Power.from_watts(200.0)) + ) + + await mockgrid.mock_resampler.send_meter_power([-100.0, -200.0]) + + # Test min + min_pow = await engine_min_rx.receive() + assert ( + min_pow + and min_pow.value + and min_pow.value.isclose(Power.from_watts(-200.0)) + ) - await engine_min._stop() # pylint: disable=protected-access - await mockgrid.cleanup() - await logical_meter.stop() - await grid.stop() + # Test max + max_pow = await engine_max_rx.receive() + assert ( + max_pow + and max_pow.value + and max_pow.value.isclose(Power.from_watts(-100.0)) + ) async def test_formula_composition_min_max_const( self, mocker: MockerFixture ) -> None: """Test the compositing formulas and constants with the min and max functions.""" - mockgrid = MockMicrogrid(grid_meter=True) - await mockgrid.start(mocker) - - logical_meter = microgrid.logical_meter() - grid = microgrid.grid() - engine_min = grid.power.min(Power.zero()).build("grid_power_min") - engine_min_rx = engine_min.new_receiver() - engine_max = grid.power.max(Power.zero()).build("grid_power_max") - engine_max_rx = engine_max.new_receiver() - - await mockgrid.mock_resampler.send_meter_power([100.0]) - - # Test min - min_pow = await engine_min_rx.receive() - assert min_pow and min_pow.value and min_pow.value.isclose(Power.zero()) - - # Test max - max_pow = await engine_max_rx.receive() - assert ( - max_pow and max_pow.value and max_pow.value.isclose(Power.from_watts(100.0)) - ) + async with MockMicrogrid( + grid_meter=True, mocker=mocker + ) as mockgrid, AsyncExitStack() as stack: + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) + + grid = microgrid.grid() + stack.push_async_callback(grid.stop) + + engine_min = grid.power.min(Power.zero()).build("grid_power_min") + stack.push_async_callback( + engine_min._stop # pylint: disable=protected-access + ) + engine_min_rx = engine_min.new_receiver() - await mockgrid.mock_resampler.send_meter_power([-100.0]) + engine_max = grid.power.max(Power.zero()).build("grid_power_max") + stack.push_async_callback( + engine_max._stop # pylint: disable=protected-access + ) + engine_max_rx = engine_max.new_receiver() - # Test min - min_pow = await engine_min_rx.receive() - assert ( - min_pow - and min_pow.value - and min_pow.value.isclose(Power.from_watts(-100.0)) - ) + await mockgrid.mock_resampler.send_meter_power([100.0]) - # Test max - max_pow = await engine_max_rx.receive() - assert max_pow and max_pow.value and max_pow.value.isclose(Power.zero()) + # Test min + min_pow = await engine_min_rx.receive() + assert min_pow and min_pow.value and min_pow.value.isclose(Power.zero()) - await engine_min._stop() # pylint: disable=protected-access - await mockgrid.cleanup() - await logical_meter.stop() - await grid.stop() + # Test max + max_pow = await engine_max_rx.receive() + assert ( + max_pow + and max_pow.value + and max_pow.value.isclose(Power.from_watts(100.0)) + ) - async def test_formula_composition_constant(self, mocker: MockerFixture) -> None: - """Test the composition of formulas with constant values.""" - mockgrid = MockMicrogrid(grid_meter=True) - await mockgrid.start(mocker) - - logical_meter = microgrid.logical_meter() - grid = microgrid.grid() - engine_add = (grid.power + Power.from_watts(50)).build("grid_power_addition") - engine_sub = (grid.power - Power.from_watts(100)).build( - "grid_power_subtraction" - ) - engine_mul = (grid.power * 2.0).build("grid_power_multiplication") - engine_div = (grid.power / 2.0).build("grid_power_division") + await mockgrid.mock_resampler.send_meter_power([-100.0]) - await mockgrid.mock_resampler.send_meter_power([100.0]) + # Test min + min_pow = await engine_min_rx.receive() + assert ( + min_pow + and min_pow.value + and min_pow.value.isclose(Power.from_watts(-100.0)) + ) - # Test addition - grid_power_addition = await engine_add.new_receiver().receive() - assert grid_power_addition.value is not None - assert math.isclose( - grid_power_addition.value.as_watts(), - 150.0, - ) + # Test max + max_pow = await engine_max_rx.receive() + assert max_pow and max_pow.value and max_pow.value.isclose(Power.zero()) - # Test subtraction - grid_power_subtraction = await engine_sub.new_receiver().receive() - assert grid_power_subtraction.value is not None - assert math.isclose( - grid_power_subtraction.value.as_watts(), - 0.0, - ) + async def test_formula_composition_constant(self, mocker: MockerFixture) -> None: + """Test the composition of formulas with constant values.""" + async with MockMicrogrid( + grid_meter=True, mocker=mocker + ) as mockgrid, AsyncExitStack() as stack: + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) + grid = microgrid.grid() + stack.push_async_callback(grid.stop) + engine_add = (grid.power + Power.from_watts(50)).build( + "grid_power_addition" + ) + stack.push_async_callback( + engine_add._stop # pylint: disable=protected-access + ) + engine_sub = (grid.power - Power.from_watts(100)).build( + "grid_power_subtraction" + ) + stack.push_async_callback( + engine_sub._stop # pylint: disable=protected-access + ) + engine_mul = (grid.power * 2.0).build("grid_power_multiplication") + stack.push_async_callback( + engine_mul._stop # pylint: disable=protected-access + ) + engine_div = (grid.power / 2.0).build("grid_power_division") + stack.push_async_callback( + engine_div._stop # pylint: disable=protected-access + ) - # Test multiplication - grid_power_multiplication = await engine_mul.new_receiver().receive() - assert grid_power_multiplication.value is not None - assert math.isclose( - grid_power_multiplication.value.as_watts(), - 200.0, - ) + await mockgrid.mock_resampler.send_meter_power([100.0]) - # Test division - grid_power_division = await engine_div.new_receiver().receive() - assert grid_power_division.value is not None - assert math.isclose( - grid_power_division.value.as_watts(), - 50.0, - ) + # Test addition + grid_power_addition = await engine_add.new_receiver().receive() + assert grid_power_addition.value is not None + assert math.isclose( + grid_power_addition.value.as_watts(), + 150.0, + ) + + # Test subtraction + grid_power_subtraction = await engine_sub.new_receiver().receive() + assert grid_power_subtraction.value is not None + assert math.isclose( + grid_power_subtraction.value.as_watts(), + 0.0, + ) - # Test multiplication with a Quantity - with pytest.raises(RuntimeError): - engine_assert = (grid.power * Power.from_watts(2.0)).build( # type: ignore - "grid_power_multiplication" + # Test multiplication + grid_power_multiplication = await engine_mul.new_receiver().receive() + assert grid_power_multiplication.value is not None + assert math.isclose( + grid_power_multiplication.value.as_watts(), + 200.0, ) - await engine_assert.new_receiver().receive() - # Test addition with a float - with pytest.raises(RuntimeError): - engine_assert = (grid.power + 2.0).build( # type: ignore - "grid_power_multiplication" + # Test division + grid_power_division = await engine_div.new_receiver().receive() + assert grid_power_division.value is not None + assert math.isclose( + grid_power_division.value.as_watts(), + 50.0, ) - await engine_assert.new_receiver().receive() - await engine_add._stop() # pylint: disable=protected-access - await engine_sub._stop() # pylint: disable=protected-access - await engine_mul._stop() # pylint: disable=protected-access - await engine_div._stop() # pylint: disable=protected-access - await mockgrid.cleanup() - await logical_meter.stop() - await grid.stop() + # Test multiplication with a Quantity + with pytest.raises(RuntimeError): + engine_assert = (grid.power * Power.from_watts(2.0)).build( # type: ignore + "grid_power_multiplication" + ) + await engine_assert.new_receiver().receive() + + # Test addition with a float + with pytest.raises(RuntimeError): + engine_assert = (grid.power + 2.0).build( # type: ignore + "grid_power_multiplication" + ) + await engine_assert.new_receiver().receive() async def test_3_phase_formulas(self, mocker: MockerFixture) -> None: """Test 3 phase formulas current formulas and their composition.""" - mockgrid = MockMicrogrid(grid_meter=False, sample_rate_s=0.05, num_namespaces=2) + mockgrid = MockMicrogrid( + grid_meter=False, sample_rate_s=0.05, num_namespaces=2, mocker=mocker + ) mockgrid.add_batteries(3) mockgrid.add_ev_chargers(1) - await mockgrid.start(mocker) - logical_meter = microgrid.logical_meter() - ev_pool = microgrid.ev_charger_pool() - grid = microgrid.grid() - - grid_current_recv = grid.current.new_receiver() - ev_current_recv = ev_pool.current.new_receiver() - - engine = (grid.current - ev_pool.current).build("net_current") - net_current_recv = engine.new_receiver() count = 0 - - for _ in range(10): - await mockgrid.mock_resampler.send_meter_current( - [ - [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( - [[10.0 + count, 12.0 + count, 14.0 + count]] - ) - - grid_amps = await grid_current_recv.receive() - ev_amps = await ev_current_recv.receive() - net_amps = await net_current_recv.receive() - - assert ( - grid_amps.value_p1 is not None and grid_amps.value_p1.base_value > 0.0 - ) - assert ( - grid_amps.value_p2 is not None and grid_amps.value_p2.base_value > 0.0 - ) - assert ( - grid_amps.value_p3 is not None and grid_amps.value_p3.base_value > 0.0 - ) - assert ev_amps.value_p1 is not None and ev_amps.value_p1.base_value > 0.0 - assert ev_amps.value_p2 is not None and ev_amps.value_p2.base_value > 0.0 - assert ev_amps.value_p3 is not None and ev_amps.value_p3.base_value > 0.0 - assert net_amps.value_p1 is not None and net_amps.value_p1.base_value > 0.0 - assert net_amps.value_p2 is not None and net_amps.value_p2.base_value > 0.0 - assert net_amps.value_p3 is not None and net_amps.value_p3.base_value > 0.0 - - assert ( - net_amps.value_p1.base_value - == grid_amps.value_p1.base_value - ev_amps.value_p1.base_value - ) - assert ( - net_amps.value_p2.base_value - == grid_amps.value_p2.base_value - ev_amps.value_p2.base_value - ) - assert ( - net_amps.value_p3.base_value - == grid_amps.value_p3.base_value - ev_amps.value_p3.base_value - ) - count += 1 - - await mockgrid.cleanup() - await engine._stop() # pylint: disable=protected-access - await logical_meter.stop() - await ev_pool.stop() - await grid.stop() + async with mockgrid, AsyncExitStack() as stack: + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) + + ev_pool = microgrid.ev_charger_pool() + stack.push_async_callback(ev_pool.stop) + + grid = microgrid.grid() + stack.push_async_callback(grid.stop) + + grid_current_recv = grid.current.new_receiver() + ev_current_recv = ev_pool.current.new_receiver() + + engine = (grid.current - ev_pool.current).build("net_current") + stack.push_async_callback(engine._stop) # pylint: disable=protected-access + net_current_recv = engine.new_receiver() + + for _ in range(10): + await mockgrid.mock_resampler.send_meter_current( + [ + [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( + [[10.0 + count, 12.0 + count, 14.0 + count]] + ) + + grid_amps = await grid_current_recv.receive() + ev_amps = await ev_current_recv.receive() + net_amps = await net_current_recv.receive() + + assert ( + grid_amps.value_p1 is not None + and grid_amps.value_p1.base_value > 0.0 + ) + assert ( + grid_amps.value_p2 is not None + and grid_amps.value_p2.base_value > 0.0 + ) + assert ( + grid_amps.value_p3 is not None + and grid_amps.value_p3.base_value > 0.0 + ) + assert ( + ev_amps.value_p1 is not None and ev_amps.value_p1.base_value > 0.0 + ) + assert ( + ev_amps.value_p2 is not None and ev_amps.value_p2.base_value > 0.0 + ) + assert ( + ev_amps.value_p3 is not None and ev_amps.value_p3.base_value > 0.0 + ) + assert ( + net_amps.value_p1 is not None and net_amps.value_p1.base_value > 0.0 + ) + assert ( + net_amps.value_p2 is not None and net_amps.value_p2.base_value > 0.0 + ) + assert ( + net_amps.value_p3 is not None and net_amps.value_p3.base_value > 0.0 + ) + + assert ( + net_amps.value_p1.base_value + == grid_amps.value_p1.base_value - ev_amps.value_p1.base_value + ) + assert ( + net_amps.value_p2.base_value + == grid_amps.value_p2.base_value - ev_amps.value_p2.base_value + ) + assert ( + net_amps.value_p3.base_value + == grid_amps.value_p3.base_value - ev_amps.value_p3.base_value + ) + count += 1 assert count == 10 diff --git a/tests/timeseries/mock_microgrid.py b/tests/timeseries/mock_microgrid.py index 8a5e4e71a..cbf4d90ee 100644 --- a/tests/timeseries/mock_microgrid.py +++ b/tests/timeseries/mock_microgrid.py @@ -3,11 +3,12 @@ """A configurable mock microgrid for testing logical meter formulas.""" +from __future__ import annotations import asyncio -import typing from collections.abc import Callable from datetime import datetime, timedelta, timezone +from typing import Coroutine from pytest_mock import MockerFixture @@ -62,6 +63,7 @@ def __init__( # pylint: disable=too-many-arguments num_namespaces: int = 1, fuse: Fuse | None = Fuse(Current.from_amperes(10_000.0)), graph: _MicrogridComponentGraph | None = None, + mocker: MockerFixture | None = None, ): """Create a new instance. @@ -78,10 +80,12 @@ def __init__( # pylint: disable=too-many-arguments fuse: optional, the fuse to use for the grid connection. graph: optional, a graph of components to use instead of the default grid layout. If specified, grid_meter must be None. + mocker: optional, a mocker to pass to the mock client and mock resampler. Raises: ValueError: if both grid_meter and graph are specified. """ + self._mocker = mocker if grid_meter is not None and graph is not None: raise ValueError("grid_meter and graph are mutually exclusive") @@ -151,7 +155,7 @@ def inverters(comp_type: InverterType) -> list[int]: self.evc_component_states: dict[int, EVChargerComponentState] = {} self.evc_cable_states: dict[int, EVChargerCableState] = {} - self._streaming_coros: list[typing.Coroutine[None, None, None]] = [] + self._streaming_coros: list[Coroutine[None, None, None]] = [] self._streaming_tasks: list[asyncio.Task[None]] = [] if grid_meter: @@ -163,9 +167,22 @@ def inverters(comp_type: InverterType) -> list[int]: self.meter_ids.append(self._grid_meter_id) self._start_meter_streaming(self._grid_meter_id) - async def start(self, mocker: MockerFixture) -> None: + async def start(self, mocker: MockerFixture | None = None) -> None: """Init the mock microgrid client and start the mock resampler.""" - self.init_mock_client(lambda mock_client: mock_client.initialize(mocker)) + # Return if it is already started + if hasattr(self, "mock_client") or hasattr(self, "mock_resampler"): + return + + if mocker is None: + mocker = self._mocker + assert mocker is not None, "A mocker must be set at init or start time" + + # This binding to a local is needed because Python uses late binding for + # closures and `mocker` could be bound to `None` again after the lambda is + # created. See: + # https://mypy.readthedocs.io/en/stable/common_issues.html#narrowing-and-inner-functions + local_mocker = mocker + self.init_mock_client(lambda mock_client: mock_client.initialize(local_mocker)) self.mock_resampler = MockResampler( mocker, ResamplerConfig(timedelta(seconds=self._sample_rate_s)), @@ -553,3 +570,12 @@ async def cleanup(self) -> None: await cancel_and_await(task) microgrid.connection_manager._CONNECTION_MANAGER = None # pylint: enable=protected-access + + async def __aenter__(self) -> MockMicrogrid: + """Enter context manager.""" + await self.start() + return self + + async def __aexit__(self, exc_type: None, exc_val: None, exc_tb: None) -> None: + """Exit context manager.""" + await self.cleanup() diff --git a/tests/timeseries/test_ev_charger_pool.py b/tests/timeseries/test_ev_charger_pool.py index d41550723..5e6620a14 100644 --- a/tests/timeseries/test_ev_charger_pool.py +++ b/tests/timeseries/test_ev_charger_pool.py @@ -5,6 +5,8 @@ import asyncio +from contextlib import AsyncExitStack, asynccontextmanager +from typing import Any, AsyncIterator from pytest_mock import MockerFixture @@ -21,78 +23,87 @@ from tests.timeseries.mock_microgrid import MockMicrogrid +@asynccontextmanager +async def new_state_tracker(*args: Any, **kwargs: Any) -> AsyncIterator[StateTracker]: + """Create a state tracker.""" + tracker = StateTracker(*args, **kwargs) + try: + yield tracker + finally: + await tracker.stop() + + class TestEVChargerPool: """Tests for the `EVChargerPool`.""" async def test_state_updates(self, mocker: MockerFixture) -> None: """Test ev charger state updates are visible.""" mockgrid = MockMicrogrid( - grid_meter=False, api_client_streaming=True, sample_rate_s=0.01 + grid_meter=False, + api_client_streaming=True, + sample_rate_s=0.01, + mocker=mocker, ) mockgrid.add_ev_chargers(5) - await mockgrid.start(mocker) - - state_tracker = StateTracker(set(mockgrid.evc_ids)) - await asyncio.sleep(0.05) - async def check_states( - expected: dict[int, EVChargerState], - ) -> None: - await mockgrid.send_ev_charger_data( - [0.0] * 5 # for testing status updates, the values don't matter. - ) + async with mockgrid, new_state_tracker(set(mockgrid.evc_ids)) as state_tracker: await asyncio.sleep(0.05) - for comp_id, exp_state in expected.items(): - assert state_tracker.get(comp_id) == exp_state - - # check that all chargers are in idle state. - expected_states = {evc_id: EVChargerState.IDLE for evc_id in mockgrid.evc_ids} - assert len(expected_states) == 5 - await check_states(expected_states) - - # check that EV_PLUGGED state gets set - evc_2_id = mockgrid.evc_ids[2] - mockgrid.evc_cable_states[evc_2_id] = EVChargerCableState.EV_PLUGGED - mockgrid.evc_component_states[evc_2_id] = EVChargerComponentState.READY - expected_states[evc_2_id] = EVChargerState.EV_PLUGGED - await check_states(expected_states) - - # check that EV_LOCKED state gets set - evc_3_id = mockgrid.evc_ids[3] - mockgrid.evc_cable_states[evc_3_id] = EVChargerCableState.EV_LOCKED - mockgrid.evc_component_states[evc_3_id] = EVChargerComponentState.READY - expected_states[evc_3_id] = EVChargerState.EV_LOCKED - await check_states(expected_states) - - # check that ERROR state gets set - evc_1_id = mockgrid.evc_ids[1] - mockgrid.evc_cable_states[evc_1_id] = EVChargerCableState.EV_LOCKED - mockgrid.evc_component_states[evc_1_id] = EVChargerComponentState.ERROR - expected_states[evc_1_id] = EVChargerState.ERROR - await check_states(expected_states) - - await state_tracker.stop() - await mockgrid.cleanup() + + async def check_states( + expected: dict[int, EVChargerState], + ) -> None: + await mockgrid.send_ev_charger_data( + [0.0] * 5 # for testing status updates, the values don't matter. + ) + await asyncio.sleep(0.05) + for comp_id, exp_state in expected.items(): + assert state_tracker.get(comp_id) == exp_state + + # check that all chargers are in idle state. + expected_states = { + evc_id: EVChargerState.IDLE for evc_id in mockgrid.evc_ids + } + assert len(expected_states) == 5 + await check_states(expected_states) + + # check that EV_PLUGGED state gets set + evc_2_id = mockgrid.evc_ids[2] + mockgrid.evc_cable_states[evc_2_id] = EVChargerCableState.EV_PLUGGED + mockgrid.evc_component_states[evc_2_id] = EVChargerComponentState.READY + expected_states[evc_2_id] = EVChargerState.EV_PLUGGED + await check_states(expected_states) + + # check that EV_LOCKED state gets set + evc_3_id = mockgrid.evc_ids[3] + mockgrid.evc_cable_states[evc_3_id] = EVChargerCableState.EV_LOCKED + mockgrid.evc_component_states[evc_3_id] = EVChargerComponentState.READY + expected_states[evc_3_id] = EVChargerState.EV_LOCKED + await check_states(expected_states) + + # check that ERROR state gets set + evc_1_id = mockgrid.evc_ids[1] + mockgrid.evc_cable_states[evc_1_id] = EVChargerCableState.EV_LOCKED + mockgrid.evc_component_states[evc_1_id] = EVChargerComponentState.ERROR + expected_states[evc_1_id] = EVChargerState.ERROR + await check_states(expected_states) async def test_ev_power( # pylint: disable=too-many-locals self, mocker: MockerFixture, ) -> None: """Test the ev power formula.""" - mockgrid = MockMicrogrid(grid_meter=False) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_ev_chargers(3) - await mockgrid.start(mocker) - - ev_pool = microgrid.ev_charger_pool() - power_receiver = ev_pool.power.new_receiver() - await mockgrid.mock_resampler.send_evc_power([2.0, 4.0, 10.0]) - assert (await power_receiver.receive()).value == Power.from_watts(16.0) + async with mockgrid: + ev_pool = microgrid.ev_charger_pool() + power_receiver = ev_pool.power.new_receiver() - await mockgrid.mock_resampler.send_evc_power([2.0, 4.0, -10.0]) - assert (await power_receiver.receive()).value == Power.from_watts(-4.0) + await mockgrid.mock_resampler.send_evc_power([2.0, 4.0, 10.0]) + assert (await power_receiver.receive()).value == Power.from_watts(16.0) - await mockgrid.cleanup() + await mockgrid.mock_resampler.send_evc_power([2.0, 4.0, -10.0]) + assert (await power_receiver.receive()).value == Power.from_watts(-4.0) async def test_ev_component_data(self, mocker: MockerFixture) -> None: """Test the component_data method of EVChargerPool.""" @@ -100,101 +111,99 @@ async def test_ev_component_data(self, mocker: MockerFixture) -> None: grid_meter=False, api_client_streaming=True, sample_rate_s=0.05, + mocker=mocker, ) mockgrid.add_ev_chargers(1) - await mockgrid.start(mocker) + async with mockgrid, AsyncExitStack() as stack: + evc_id = mockgrid.evc_ids[0] + ev_pool = microgrid.ev_charger_pool() + stack.push_async_callback(ev_pool.stop) - evc_id = mockgrid.evc_ids[0] - ev_pool = microgrid.ev_charger_pool() + recv = ev_pool.component_data(evc_id) - recv = ev_pool.component_data(evc_id) - - await mockgrid.send_ev_charger_data( - [0.0] # only the status gets used from this. - ) - await asyncio.sleep(0.05) - await mockgrid.mock_resampler.send_evc_current([[2, 3, 5]]) - status = await recv.receive() - assert ( - status.current.value_p1, - status.current.value_p2, - status.current.value_p3, - ) == ( - Current.from_amperes(2), - Current.from_amperes(3), - Current.from_amperes(5), - ) - assert status.state == EVChargerState.MISSING - - await mockgrid.send_ev_charger_data( - [0.0] # only the status gets used from this. - ) - await asyncio.sleep(0.05) - await mockgrid.mock_resampler.send_evc_current([[2, 3, None]]) - status = await recv.receive() - assert ( - status.current.value_p1, - status.current.value_p2, - status.current.value_p3, - ) == ( - Current.from_amperes(2), - Current.from_amperes(3), - None, - ) - assert status.state == EVChargerState.IDLE + await mockgrid.send_ev_charger_data( + [0.0] # only the status gets used from this. + ) + await asyncio.sleep(0.05) + await mockgrid.mock_resampler.send_evc_current([[2, 3, 5]]) + status = await recv.receive() + assert ( + status.current.value_p1, + status.current.value_p2, + status.current.value_p3, + ) == ( + Current.from_amperes(2), + Current.from_amperes(3), + Current.from_amperes(5), + ) + assert status.state == EVChargerState.MISSING - await mockgrid.send_ev_charger_data( - [0.0] # only the status gets used from this. - ) - await asyncio.sleep(0.05) - await mockgrid.mock_resampler.send_evc_current([[None, None, None]]) - status = await recv.receive() - assert ( - status.current.value_p1, - status.current.value_p2, - status.current.value_p3, - ) == ( - None, - None, - None, - ) - assert status.state == EVChargerState.MISSING + await mockgrid.send_ev_charger_data( + [0.0] # only the status gets used from this. + ) + await asyncio.sleep(0.05) + await mockgrid.mock_resampler.send_evc_current([[2, 3, None]]) + status = await recv.receive() + assert ( + status.current.value_p1, + status.current.value_p2, + status.current.value_p3, + ) == ( + Current.from_amperes(2), + Current.from_amperes(3), + None, + ) + assert status.state == EVChargerState.IDLE - mockgrid.evc_cable_states[evc_id] = EVChargerCableState.EV_PLUGGED - await mockgrid.send_ev_charger_data( - [0.0] # only the status gets used from this. - ) - await asyncio.sleep(0.05) - await mockgrid.mock_resampler.send_evc_current([[None, None, None]]) - status = await recv.receive() - assert ( - status.current.value_p1, - status.current.value_p2, - status.current.value_p3, - ) == ( - None, - None, - None, - ) - assert status.state == EVChargerState.MISSING + await mockgrid.send_ev_charger_data( + [0.0] # only the status gets used from this. + ) + await asyncio.sleep(0.05) + await mockgrid.mock_resampler.send_evc_current([[None, None, None]]) + status = await recv.receive() + assert ( + status.current.value_p1, + status.current.value_p2, + status.current.value_p3, + ) == ( + None, + None, + None, + ) + assert status.state == EVChargerState.MISSING - await mockgrid.send_ev_charger_data( - [0.0] # only the status gets used from this. - ) - await asyncio.sleep(0.05) - await mockgrid.mock_resampler.send_evc_current([[4, None, None]]) - status = await recv.receive() - assert ( - status.current.value_p1, - status.current.value_p2, - status.current.value_p3, - ) == ( - Current.from_amperes(4), - None, - None, - ) - assert status.state == EVChargerState.EV_PLUGGED + mockgrid.evc_cable_states[evc_id] = EVChargerCableState.EV_PLUGGED + await mockgrid.send_ev_charger_data( + [0.0] # only the status gets used from this. + ) + await asyncio.sleep(0.05) + await mockgrid.mock_resampler.send_evc_current([[None, None, None]]) + status = await recv.receive() + assert ( + status.current.value_p1, + status.current.value_p2, + status.current.value_p3, + ) == ( + None, + None, + None, + ) + assert status.state == EVChargerState.MISSING - await mockgrid.cleanup() - await ev_pool.stop() + await mockgrid.send_ev_charger_data( + [0.0] # only the status gets used from this. + ) + await asyncio.sleep(0.05) + await mockgrid.mock_resampler.send_evc_current([[4, None, None]]) + status = await recv.receive() + assert ( + status.current.value_p1, + status.current.value_p2, + status.current.value_p3, + ) == ( + Current.from_amperes(4), + None, + None, + ) + assert status.state == EVChargerState.EV_PLUGGED diff --git a/tests/timeseries/test_formula_formatter.py b/tests/timeseries/test_formula_formatter.py index c55a85837..5ac1ef60b 100644 --- a/tests/timeseries/test_formula_formatter.py +++ b/tests/timeseries/test_formula_formatter.py @@ -4,6 +4,8 @@ """Tests for the FormulaFormatter.""" +from contextlib import AsyncExitStack + from frequenz.channels import Broadcast from pytest_mock import MockerFixture @@ -112,22 +114,24 @@ def test_functions(self) -> None: async def test_higher_order_formula(self, mocker: MockerFixture) -> None: """Test that the formula is formatted correctly for a higher-order formula.""" - mockgrid = MockMicrogrid(grid_meter=False) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_batteries(3) mockgrid.add_ev_chargers(1) mockgrid.add_solar_inverters(2) - await mockgrid.start(mocker) - logical_meter = microgrid.logical_meter() - grid = microgrid.grid() - assert str(grid.power) == "#36 + #7 + #47 + #17 + #57 + #27" + async with mockgrid, AsyncExitStack() as stack: + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) + + grid = microgrid.grid() + stack.push_async_callback(grid.stop) - composed_formula = (grid.power - logical_meter.pv_power).build("grid_minus_pv") - assert ( - str(composed_formula) - == "[grid-power](#36 + #7 + #47 + #17 + #57 + #27) - [pv-power](#57 + #47)" - ) + assert str(grid.power) == "#36 + #7 + #47 + #17 + #57 + #27" - await mockgrid.cleanup() - await logical_meter.stop() - await grid.stop() + composed_formula = (grid.power - logical_meter.pv_power).build( + "grid_minus_pv" + ) + assert ( + str(composed_formula) + == "[grid-power](#36 + #7 + #47 + #17 + #57 + #27) - [pv-power](#57 + #47)" + ) diff --git a/tests/timeseries/test_frequency_streaming.py b/tests/timeseries/test_frequency_streaming.py index 6f01be100..1332aec4b 100644 --- a/tests/timeseries/test_frequency_streaming.py +++ b/tests/timeseries/test_frequency_streaming.py @@ -49,36 +49,36 @@ async def test_grid_frequency_none(mocker: MockerFixture) -> None: async def test_grid_frequency_1(mocker: MockerFixture) -> None: """Test the grid frequency formula.""" - mockgrid = MockMicrogrid(grid_meter=True) + mockgrid = MockMicrogrid(grid_meter=True, mocker=mocker) mockgrid.add_batteries(2) mockgrid.add_solar_inverters(1) - await mockgrid.start(mocker) - grid_freq = microgrid.frequency() - grid_freq_recv = grid_freq.new_receiver() - - assert grid_freq._task is not None - # We have to wait for the metric request to be sent - await grid_freq._task - # And consumed - await asyncio.sleep(0) - - results = [] - grid_meter_data = [] - for count in range(10): - freq = float(50.0 + (1 if count % 2 == 0 else -1) * 0.01) - await mockgrid.mock_client.send( - component_data_wrapper.MeterDataWrapper( - mockgrid.meter_ids[0], datetime.now(tz=timezone.utc), frequency=freq + async with mockgrid: + grid_freq = microgrid.frequency() + grid_freq_recv = grid_freq.new_receiver() + + assert grid_freq._task is not None + # We have to wait for the metric request to be sent + await grid_freq._task + # And consumed + await asyncio.sleep(0) + + results = [] + grid_meter_data = [] + for count in range(10): + freq = float(50.0 + (1 if count % 2 == 0 else -1) * 0.01) + await mockgrid.mock_client.send( + component_data_wrapper.MeterDataWrapper( + mockgrid.meter_ids[0], datetime.now(tz=timezone.utc), frequency=freq + ) ) - ) - grid_meter_data.append(Frequency.from_hertz(freq)) - val = await grid_freq_recv.receive() - assert val is not None and val.value is not None - assert val.value.as_hertz() == freq - results.append(val.value) - await mockgrid.cleanup() + grid_meter_data.append(Frequency.from_hertz(freq)) + val = await grid_freq_recv.receive() + assert val is not None and val.value is not None + assert val.value.as_hertz() == freq + results.append(val.value) + assert equal_float_lists(results, grid_meter_data) @@ -86,36 +86,37 @@ async def test_grid_frequency_no_grid_meter_no_consumer_meter( mocker: MockerFixture, ) -> None: """Test the grid frequency formula without a grid side meter.""" - mockgrid = MockMicrogrid(grid_meter=False) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_consumer_meters() mockgrid.add_batteries(1, no_meter=False) mockgrid.add_batteries(1, no_meter=False) - await mockgrid.start(mocker) - grid_freq = microgrid.frequency() - grid_freq_recv = grid_freq.new_receiver() - # We have to wait for the metric request to be sent - assert grid_freq._task is not None - await grid_freq._task - # And consumed - await asyncio.sleep(0) - - results = [] - meter_data = [] - for count in range(10): - freq = float(50.0 + (1 if count % 2 == 0 else -1) * 0.01) - await mockgrid.mock_client.send( - component_data_wrapper.MeterDataWrapper( - mockgrid.meter_ids[0], datetime.now(tz=timezone.utc), frequency=freq + async with mockgrid: + grid_freq = microgrid.frequency() + + grid_freq_recv = grid_freq.new_receiver() + # We have to wait for the metric request to be sent + assert grid_freq._task is not None + await grid_freq._task + # And consumed + await asyncio.sleep(0) + + results = [] + meter_data = [] + for count in range(10): + freq = float(50.0 + (1 if count % 2 == 0 else -1) * 0.01) + await mockgrid.mock_client.send( + component_data_wrapper.MeterDataWrapper( + mockgrid.meter_ids[0], datetime.now(tz=timezone.utc), frequency=freq + ) ) - ) - meter_data.append(Frequency.from_hertz(freq)) + meter_data.append(Frequency.from_hertz(freq)) + + val = await grid_freq_recv.receive() + assert val is not None and val.value is not None + assert val.value.as_hertz() == freq + results.append(val.value) - val = await grid_freq_recv.receive() - assert val is not None and val.value is not None - assert val.value.as_hertz() == freq - results.append(val.value) - await mockgrid.cleanup() assert equal_float_lists(results, meter_data) @@ -123,35 +124,36 @@ async def test_grid_frequency_no_grid_meter( mocker: MockerFixture, ) -> None: """Test the grid frequency formula without a grid side meter.""" - mockgrid = MockMicrogrid(grid_meter=False) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_batteries(1, no_meter=False) mockgrid.add_batteries(1, no_meter=True) - await mockgrid.start(mocker) - grid_freq = microgrid.frequency() - grid_freq_recv = grid_freq.new_receiver() - # We have to wait for the metric request to be sent - assert grid_freq._task is not None - await grid_freq._task - # And consumed - await asyncio.sleep(0) - - results = [] - meter_data = [] - for count in range(10): - freq = float(50.0 + (1 if count % 2 == 0 else -1) * 0.01) - await mockgrid.mock_client.send( - component_data_wrapper.MeterDataWrapper( - mockgrid.meter_ids[0], datetime.now(tz=timezone.utc), frequency=freq + async with mockgrid: + grid_freq = microgrid.frequency() + + grid_freq_recv = grid_freq.new_receiver() + # We have to wait for the metric request to be sent + assert grid_freq._task is not None + await grid_freq._task + # And consumed + await asyncio.sleep(0) + + results = [] + meter_data = [] + for count in range(10): + freq = float(50.0 + (1 if count % 2 == 0 else -1) * 0.01) + await mockgrid.mock_client.send( + component_data_wrapper.MeterDataWrapper( + mockgrid.meter_ids[0], datetime.now(tz=timezone.utc), frequency=freq + ) ) - ) - meter_data.append(Frequency.from_hertz(freq)) + meter_data.append(Frequency.from_hertz(freq)) + + val = await grid_freq_recv.receive() + assert val is not None and val.value is not None + assert val.value.as_hertz() == freq + results.append(val.value) - val = await grid_freq_recv.receive() - assert val is not None and val.value is not None - assert val.value.as_hertz() == freq - results.append(val.value) - await mockgrid.cleanup() assert equal_float_lists(results, meter_data) @@ -159,35 +161,35 @@ async def test_grid_frequency_only_inverter( mocker: MockerFixture, ) -> None: """Test the grid frequency formula without any meter but only inverters.""" - mockgrid = MockMicrogrid(grid_meter=False) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_batteries(2, no_meter=True) - await mockgrid.start(mocker) - grid_freq = microgrid.frequency() - grid_freq_recv = grid_freq.new_receiver() - # We have to wait for the metric request to be sent - assert grid_freq._task is not None - await grid_freq._task - # And consumed - await asyncio.sleep(0) - - results = [] - meter_data = [] - for count in range(10): - freq = float(50.0 + (1 if count % 2 == 0 else -1) * 0.01) - - await mockgrid.mock_client.send( - component_data_wrapper.InverterDataWrapper( - mockgrid.battery_inverter_ids[0], - datetime.now(tz=timezone.utc), - frequency=freq, + async with mockgrid: + grid_freq = microgrid.frequency() + grid_freq_recv = grid_freq.new_receiver() + # We have to wait for the metric request to be sent + assert grid_freq._task is not None + await grid_freq._task + # And consumed + await asyncio.sleep(0) + + results = [] + meter_data = [] + for count in range(10): + freq = float(50.0 + (1 if count % 2 == 0 else -1) * 0.01) + + await mockgrid.mock_client.send( + component_data_wrapper.InverterDataWrapper( + mockgrid.battery_inverter_ids[0], + datetime.now(tz=timezone.utc), + frequency=freq, + ) ) - ) - meter_data.append(Frequency.from_hertz(freq)) - val = await grid_freq_recv.receive() - assert val is not None and val.value is not None - assert val.value.as_hertz() == freq - results.append(val.value) - await mockgrid.cleanup() + meter_data.append(Frequency.from_hertz(freq)) + val = await grid_freq_recv.receive() + assert val is not None and val.value is not None + assert val.value.as_hertz() == freq + results.append(val.value) + assert equal_float_lists(results, meter_data) diff --git a/tests/timeseries/test_logical_meter.py b/tests/timeseries/test_logical_meter.py index 8cab4e69e..14a366030 100644 --- a/tests/timeseries/test_logical_meter.py +++ b/tests/timeseries/test_logical_meter.py @@ -4,6 +4,8 @@ """Tests for the logical meter.""" +from contextlib import AsyncExitStack + from pytest_mock import MockerFixture from frequenz.sdk import microgrid @@ -20,157 +22,185 @@ class TestLogicalMeter: # pylint: disable=too-many-public-methods async def test_chp_power(self, mocker: MockerFixture) -> None: """Test the chp power formula.""" - mockgrid = MockMicrogrid(grid_meter=False) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_chps(1) mockgrid.add_batteries(2) - await mockgrid.start(mocker) - logical_meter = microgrid.logical_meter() - chp_power_receiver = logical_meter.chp_power.new_receiver() + async with mockgrid, AsyncExitStack() as stack: + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) + + chp_power_receiver = logical_meter.chp_power.new_receiver() - 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) + 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) - await mockgrid.mock_resampler.send_meter_power([-12.0, None, 10.2]) - assert (await chp_power_receiver.receive()).value == Power.from_watts(-12.0) + await mockgrid.mock_resampler.send_meter_power([-12.0, None, 10.2]) + assert (await chp_power_receiver.receive()).value == Power.from_watts(-12.0) async def test_pv_power(self, mocker: MockerFixture) -> None: """Test the pv power formula.""" - mockgrid = MockMicrogrid(grid_meter=False) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_solar_inverters(2) - await mockgrid.start(mocker) - logical_meter = microgrid.logical_meter() - pv_power_receiver = logical_meter.pv_power.new_receiver() + async with mockgrid, AsyncExitStack() as stack: + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) + pv_power_receiver = logical_meter.pv_power.new_receiver() - await mockgrid.mock_resampler.send_meter_power([-1.0, -2.0]) - assert (await pv_power_receiver.receive()).value == Power.from_watts(-3.0) + await mockgrid.mock_resampler.send_meter_power([-1.0, -2.0]) + assert (await pv_power_receiver.receive()).value == Power.from_watts(-3.0) async def test_pv_power_no_meter(self, mocker: MockerFixture) -> None: """Test the pv power formula.""" - mockgrid = MockMicrogrid(grid_meter=False) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_solar_inverters(2, no_meter=True) - await mockgrid.start(mocker) - logical_meter = microgrid.logical_meter() - pv_power_receiver = logical_meter.pv_power.new_receiver() + async with mockgrid, AsyncExitStack() as stack: + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) + pv_power_receiver = logical_meter.pv_power.new_receiver() - await mockgrid.mock_resampler.send_pv_inverter_power([-1.0, -2.0]) - assert (await pv_power_receiver.receive()).value == Power.from_watts(-3.0) + await mockgrid.mock_resampler.send_pv_inverter_power([-1.0, -2.0]) + assert (await pv_power_receiver.receive()).value == Power.from_watts(-3.0) async def test_pv_power_no_pv_components(self, mocker: MockerFixture) -> None: """Test the pv power formula without having any pv components.""" - mockgrid = MockMicrogrid(grid_meter=True) - await mockgrid.start(mocker) - - logical_meter = microgrid.logical_meter() - pv_power_receiver = logical_meter.pv_power.new_receiver() + async with MockMicrogrid( + grid_meter=True, mocker=mocker + ) as mockgrid, AsyncExitStack() as stack: + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) + pv_power_receiver = logical_meter.pv_power.new_receiver() - await mockgrid.mock_resampler.send_non_existing_component_value() - assert (await pv_power_receiver.receive()).value == Power.zero() + await mockgrid.mock_resampler.send_non_existing_component_value() + assert (await pv_power_receiver.receive()).value == Power.zero() async def test_consumer_power_grid_meter(self, mocker: MockerFixture) -> None: """Test the consumer power formula with a grid meter.""" - mockgrid = MockMicrogrid(grid_meter=True) + mockgrid = MockMicrogrid(grid_meter=True, mocker=mocker) 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() + async with mockgrid, AsyncExitStack() as stack: + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) + consumer_power_receiver = logical_meter.consumer_power.new_receiver() - 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(6.0) + 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( + 6.0 + ) async def test_consumer_power_no_grid_meter(self, mocker: MockerFixture) -> None: """Test the consumer power formula without a grid meter.""" - mockgrid = MockMicrogrid(grid_meter=False) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_consumer_meters() 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() + async with mockgrid, AsyncExitStack() as stack: + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) + consumer_power_receiver = logical_meter.consumer_power.new_receiver() - 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) + 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 = MockMicrogrid(grid_meter=False, mocker=mocker) 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() + async with mockgrid, AsyncExitStack() as stack: + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) + 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) + 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) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_solar_inverters(2) mockgrid.add_chps(2) - await mockgrid.start(mocker) - logical_meter = microgrid.logical_meter() - producer_power_receiver = logical_meter.producer_power.new_receiver() + async with mockgrid, AsyncExitStack() as stack: + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) + producer_power_receiver = logical_meter.producer_power.new_receiver() - 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) + 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_meter=False) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_solar_inverters(2) - await mockgrid.start(mocker) + async with mockgrid, AsyncExitStack() as stack: + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) + producer_power_receiver = logical_meter.producer_power.new_receiver() - logical_meter = microgrid.logical_meter() - producer_power_receiver = logical_meter.producer_power.new_receiver() - - await mockgrid.mock_resampler.send_meter_power([2.0, 3.0]) - assert (await producer_power_receiver.receive()).value == Power.from_watts(5.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 = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_chps(1, True) - await mockgrid.start(mocker) - logical_meter = microgrid.logical_meter() - producer_power_receiver = logical_meter.producer_power.new_receiver() + async with mockgrid, AsyncExitStack() as stack: + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) + producer_power_receiver = logical_meter.producer_power.new_receiver() - await mockgrid.mock_resampler.send_chp_power([2.0]) - assert (await producer_power_receiver.receive()).value == Power.from_watts(2.0) + 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_meter=False) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_consumer_meters() mockgrid.add_chps(1) - await mockgrid.start(mocker) - logical_meter = microgrid.logical_meter() - producer_power_receiver = logical_meter.producer_power.new_receiver() + async with mockgrid, AsyncExitStack() as stack: + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) + producer_power_receiver = logical_meter.producer_power.new_receiver() - await mockgrid.mock_resampler.send_meter_power([20.0, 2.0]) - assert (await producer_power_receiver.receive()).value == Power.from_watts(2.0) + await mockgrid.mock_resampler.send_meter_power([20.0, 2.0]) + assert (await producer_power_receiver.receive()).value == Power.from_watts( + 2.0 + ) async def test_no_producer_power(self, mocker: MockerFixture) -> None: """Test the producer power formula without producers.""" - mockgrid = MockMicrogrid(grid_meter=True) - await mockgrid.start(mocker) - - logical_meter = microgrid.logical_meter() - producer_power_receiver = logical_meter.producer_power.new_receiver() - - await mockgrid.mock_resampler.send_non_existing_component_value() - assert (await producer_power_receiver.receive()).value == Power.from_watts(0.0) + async with MockMicrogrid( + grid_meter=True, mocker=mocker + ) as mockgrid, AsyncExitStack() as stack: + logical_meter = microgrid.logical_meter() + stack.push_async_callback(logical_meter.stop) + producer_power_receiver = logical_meter.producer_power.new_receiver() + + await mockgrid.mock_resampler.send_non_existing_component_value() + assert (await producer_power_receiver.receive()).value == Power.from_watts( + 0.0 + ) diff --git a/tests/timeseries/test_voltage_streamer.py b/tests/timeseries/test_voltage_streamer.py index 962c27987..bdbb60bce 100644 --- a/tests/timeseries/test_voltage_streamer.py +++ b/tests/timeseries/test_voltage_streamer.py @@ -17,108 +17,102 @@ async def test_voltage_1(mocker: MockerFixture) -> None: """Test the phase-to-neutral voltage with a grid side meter.""" - mockgrid = MockMicrogrid(grid_meter=True) + mockgrid = MockMicrogrid(grid_meter=True, mocker=mocker) mockgrid.add_batteries(1, no_meter=True) mockgrid.add_batteries(1, no_meter=False) - await mockgrid.start(mocker) - voltage = microgrid.voltage() - voltage_recv = voltage.new_receiver() + async with mockgrid: + voltage = microgrid.voltage() + voltage_recv = voltage.new_receiver() - assert voltage._task is not None - # Wait for voltage requests to be sent, one request per phase. - await asyncio.sleep(0) - await asyncio.sleep(0) - await asyncio.sleep(0) + assert voltage._task is not None + # Wait for voltage requests to be sent, one request per phase. + await asyncio.sleep(0) + await asyncio.sleep(0) + await asyncio.sleep(0) - for count in range(10): - volt_delta = 1 if count % 2 == 0 else -1 - volt_phases: list[float | None] = [ - 220.0 * volt_delta, - 219.8 * volt_delta, - 220.2 * volt_delta, - ] + for count in range(10): + volt_delta = 1 if count % 2 == 0 else -1 + volt_phases: list[float | None] = [ + 220.0 * volt_delta, + 219.8 * volt_delta, + 220.2 * volt_delta, + ] - await mockgrid.mock_resampler.send_meter_voltage([volt_phases, volt_phases]) + await mockgrid.mock_resampler.send_meter_voltage([volt_phases, volt_phases]) - val = await voltage_recv.receive() - assert val is not None - assert val.value_p1 and val.value_p2 and val.value_p3 - assert val.value_p1.as_volts() == volt_phases[0] - assert val.value_p2.as_volts() == volt_phases[1] - assert val.value_p3.as_volts() == volt_phases[2] - - await mockgrid.cleanup() + val = await voltage_recv.receive() + assert val is not None + assert val.value_p1 and val.value_p2 and val.value_p3 + assert val.value_p1.as_volts() == volt_phases[0] + assert val.value_p2.as_volts() == volt_phases[1] + assert val.value_p3.as_volts() == volt_phases[2] async def test_voltage_2(mocker: MockerFixture) -> None: """Test the phase-to-neutral voltage without a grid side meter.""" - mockgrid = MockMicrogrid(grid_meter=False) + mockgrid = MockMicrogrid(grid_meter=False, mocker=mocker) mockgrid.add_batteries(1, no_meter=False) mockgrid.add_batteries(1, no_meter=True) - await mockgrid.start(mocker) - - voltage = microgrid.voltage() - voltage_recv = voltage.new_receiver() - assert voltage._task is not None - # Wait for voltage requests to be sent, one request per phase. - await asyncio.sleep(0) - await asyncio.sleep(0) - await asyncio.sleep(0) + async with mockgrid: + voltage = microgrid.voltage() + voltage_recv = voltage.new_receiver() - for count in range(10): - volt_delta = 1 if count % 2 == 0 else -1 - volt_phases: list[float | None] = [ - 220.0 * volt_delta, - 219.8 * volt_delta, - 220.2 * volt_delta, - ] + assert voltage._task is not None + # Wait for voltage requests to be sent, one request per phase. + await asyncio.sleep(0) + await asyncio.sleep(0) + await asyncio.sleep(0) - await mockgrid.mock_resampler.send_meter_voltage([volt_phases]) + for count in range(10): + volt_delta = 1 if count % 2 == 0 else -1 + volt_phases: list[float | None] = [ + 220.0 * volt_delta, + 219.8 * volt_delta, + 220.2 * volt_delta, + ] - val = await voltage_recv.receive() - assert val is not None - assert val.value_p1 and val.value_p2 and val.value_p3 - assert val.value_p1.as_volts() == volt_phases[0] - assert val.value_p2.as_volts() == volt_phases[1] - assert val.value_p3.as_volts() == volt_phases[2] + await mockgrid.mock_resampler.send_meter_voltage([volt_phases]) - await mockgrid.cleanup() + val = await voltage_recv.receive() + assert val is not None + assert val.value_p1 and val.value_p2 and val.value_p3 + assert val.value_p1.as_volts() == volt_phases[0] + assert val.value_p2.as_volts() == volt_phases[1] + assert val.value_p3.as_volts() == volt_phases[2] async def test_voltage_3(mocker: MockerFixture) -> None: """Test the phase-to-neutral voltage with None values.""" - mockgrid = MockMicrogrid(grid_meter=True) + mockgrid = MockMicrogrid(grid_meter=True, mocker=mocker) mockgrid.add_batteries(2, no_meter=False) - await mockgrid.start(mocker) - - voltage = microgrid.voltage() - voltage_recv = voltage.new_receiver() - - assert voltage._task is not None - # Wait for voltage requests to be sent, one request per phase. - await asyncio.sleep(0) - await asyncio.sleep(0) - await asyncio.sleep(0) - - for count in range(10): - volt_delta = 1 if count % 2 == 0 else -1 - volt_phases: list[float | None] = [ - 220.0 * volt_delta, - 219.8 * volt_delta, - 220.2 * volt_delta, - ] - - await mockgrid.mock_resampler.send_meter_voltage( - [volt_phases, [None, None, None], [None, 219.8, 220.2]] - ) - - val = await voltage_recv.receive() - assert val is not None - assert val.value_p1 and val.value_p2 and val.value_p3 - assert val.value_p1.as_volts() == volt_phases[0] - assert val.value_p2.as_volts() == volt_phases[1] - assert val.value_p3.as_volts() == volt_phases[2] - - await mockgrid.cleanup() + + async with mockgrid: + voltage = microgrid.voltage() + voltage_recv = voltage.new_receiver() + + assert voltage._task is not None + # Wait for voltage requests to be sent, one request per phase. + await asyncio.sleep(0) + await asyncio.sleep(0) + await asyncio.sleep(0) + + for count in range(10): + volt_delta = 1 if count % 2 == 0 else -1 + volt_phases: list[float | None] = [ + 220.0 * volt_delta, + 219.8 * volt_delta, + 220.2 * volt_delta, + ] + + await mockgrid.mock_resampler.send_meter_voltage( + [volt_phases, [None, None, None], [None, 219.8, 220.2]] + ) + + val = await voltage_recv.receive() + assert val is not None + assert val.value_p1 and val.value_p2 and val.value_p3 + assert val.value_p1.as_volts() == volt_phases[0] + assert val.value_p2.as_volts() == volt_phases[1] + assert val.value_p3.as_volts() == volt_phases[2]