Skip to content

Commit 5093900

Browse files
committed
Add tests for PVPool control methods
Signed-off-by: Sahas Subramanian <[email protected]>
1 parent a039724 commit 5093900

File tree

2 files changed

+266
-0
lines changed

2 files changed

+266
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# License: MIT
2+
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Test the PV pool control methods."""
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
# License: MIT
2+
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Test the PV pool control methods."""
5+
6+
import asyncio
7+
import typing
8+
from datetime import datetime, timedelta, timezone
9+
from unittest.mock import AsyncMock
10+
11+
import pytest
12+
13+
# pylint: disable=no-name-in-module
14+
from frequenz.api.microgrid.inverter_pb2 import ComponentState
15+
16+
# pylint: enable=no-name-in-module
17+
from frequenz.channels import Receiver
18+
from pytest_mock import MockerFixture
19+
20+
from frequenz.sdk import microgrid
21+
from frequenz.sdk.actor import ResamplerConfig, power_distributing
22+
from frequenz.sdk.microgrid._data_pipeline import _DataPipeline
23+
from frequenz.sdk.timeseries import Power
24+
from frequenz.sdk.timeseries.pv_pool import PVPoolReport
25+
26+
from ...microgrid.fixtures import _Mocks
27+
from ...utils.component_data_streamer import MockComponentDataStreamer
28+
from ...utils.component_data_wrapper import InverterDataWrapper
29+
from ..mock_microgrid import MockMicrogrid
30+
31+
32+
@pytest.fixture
33+
async def mocks(mocker: MockerFixture) -> typing.AsyncIterator[_Mocks]:
34+
"""Create the mocks."""
35+
mockgrid = MockMicrogrid(grid_meter=True)
36+
mockgrid.add_solar_inverters(4)
37+
await mockgrid.start(mocker)
38+
39+
# pylint: disable=protected-access
40+
if microgrid._data_pipeline._DATA_PIPELINE is not None:
41+
microgrid._data_pipeline._DATA_PIPELINE = None
42+
await microgrid._data_pipeline.initialize(
43+
ResamplerConfig(resampling_period=timedelta(seconds=0.1))
44+
)
45+
streamer = MockComponentDataStreamer(mockgrid.mock_client)
46+
47+
dp = typing.cast(_DataPipeline, microgrid._data_pipeline._DATA_PIPELINE)
48+
49+
yield _Mocks(
50+
mockgrid,
51+
streamer,
52+
dp._ev_power_wrapper.status_channel.new_sender(),
53+
)
54+
55+
56+
class TestPVPoolControl:
57+
"""Test control methods for the PVPool."""
58+
59+
async def _patch_data_pipeline(self, mocker: MockerFixture) -> None:
60+
mocker.patch(
61+
"frequenz.sdk.microgrid._data_pipeline._DATA_PIPELINE._pv_power_wrapper"
62+
"._pd_wait_for_data_sec",
63+
0.1,
64+
)
65+
66+
async def _init_pv_inverters(self, mocks: _Mocks) -> None:
67+
now = datetime.now(tz=timezone.utc)
68+
for idx, comp_id in enumerate(mocks.microgrid.pv_inverter_ids):
69+
mocks.streamer.start_streaming(
70+
InverterDataWrapper(
71+
comp_id,
72+
now,
73+
_component_state=ComponentState.COMPONENT_STATE_IDLE,
74+
active_power=0.0,
75+
active_power_inclusion_lower_bound=-10000.0 * (idx + 1),
76+
active_power_inclusion_upper_bound=0.0,
77+
),
78+
0.05,
79+
)
80+
81+
async def _fail_pv_inverters(self, fail_ids: list[int], mocks: _Mocks) -> None:
82+
now = datetime.now(tz=timezone.utc)
83+
for idx, comp_id in enumerate(mocks.microgrid.pv_inverter_ids):
84+
mocks.streamer.update_stream(
85+
InverterDataWrapper(
86+
comp_id,
87+
now,
88+
_component_state=(
89+
ComponentState.COMPONENT_STATE_ERROR
90+
if comp_id in fail_ids
91+
else ComponentState.COMPONENT_STATE_IDLE
92+
),
93+
active_power=0.0,
94+
active_power_inclusion_lower_bound=-10000.0 * (idx + 1),
95+
active_power_inclusion_upper_bound=0.0,
96+
),
97+
)
98+
99+
def _assert_report( # pylint: disable=too-many-arguments
100+
self,
101+
report: PVPoolReport,
102+
*,
103+
power: float | None,
104+
lower: float,
105+
upper: float,
106+
expected_result_pred: (
107+
typing.Callable[[power_distributing.Result], bool] | None
108+
) = None,
109+
) -> None:
110+
assert report.target_power == (
111+
Power.from_watts(power) if power is not None else None
112+
)
113+
assert report.bounds is not None
114+
assert report.bounds.lower == Power.from_watts(lower)
115+
assert report.bounds.upper == Power.from_watts(upper)
116+
if expected_result_pred is not None:
117+
assert report.distribution_result is not None
118+
assert expected_result_pred(report.distribution_result)
119+
120+
async def _recv_reports_until(
121+
self,
122+
bounds_rx: Receiver[PVPoolReport],
123+
check: typing.Callable[[PVPoolReport], bool],
124+
) -> None:
125+
"""Receive reports until the given condition is met."""
126+
max_reports = 10
127+
ctr = 0
128+
while ctr < max_reports:
129+
ctr += 1
130+
report = await bounds_rx.receive()
131+
if check(report):
132+
break
133+
134+
async def test_setting_power(
135+
self,
136+
mocks: _Mocks,
137+
mocker: MockerFixture,
138+
) -> None:
139+
"""Test setting power."""
140+
set_power = typing.cast(
141+
AsyncMock, microgrid.connection_manager.get().api_client.set_power
142+
)
143+
144+
await self._init_pv_inverters(mocks)
145+
await self._patch_data_pipeline(mocker)
146+
pv_pool = microgrid.pv_pool()
147+
bounds_rx = pv_pool.power_status.new_receiver()
148+
await self._recv_reports_until(
149+
bounds_rx,
150+
lambda x: x.bounds is not None and x.bounds.lower.as_watts() == -100000.0,
151+
)
152+
self._assert_report(
153+
await bounds_rx.receive(), power=None, lower=-100000.0, upper=0.0
154+
)
155+
await pv_pool.propose_power(Power.from_watts(-80000.0))
156+
await self._recv_reports_until(
157+
bounds_rx,
158+
lambda x: x.target_power is not None
159+
and x.target_power.as_watts() == -80000.0,
160+
)
161+
self._assert_report(
162+
await bounds_rx.receive(), power=-80000.0, lower=-100000.0, upper=0.0
163+
)
164+
await asyncio.sleep(0.0)
165+
166+
# Components are set initial power
167+
assert set_power.call_count == 4
168+
inv_ids = mocks.microgrid.pv_inverter_ids
169+
assert sorted(set_power.call_args_list, key=lambda x: x.args[0]) == [
170+
mocker.call(inv_ids[0], -10000.0),
171+
mocker.call(inv_ids[1], -20000.0),
172+
mocker.call(inv_ids[2], -25000.0),
173+
mocker.call(inv_ids[3], -25000.0),
174+
]
175+
176+
set_power.reset_mock()
177+
await pv_pool.propose_power(Power.from_watts(-4000.0))
178+
await self._recv_reports_until(
179+
bounds_rx,
180+
lambda x: x.target_power is not None
181+
and x.target_power.as_watts() == -4000.0,
182+
)
183+
self._assert_report(
184+
await bounds_rx.receive(), power=-4000.0, lower=-100000.0, upper=0.0
185+
)
186+
await asyncio.sleep(0.0)
187+
188+
# Components are set initial power
189+
assert set_power.call_count == 4
190+
inv_ids = mocks.microgrid.pv_inverter_ids
191+
assert sorted(set_power.call_args_list, key=lambda x: x.args[0]) == [
192+
mocker.call(inv_ids[0], -1000.0),
193+
mocker.call(inv_ids[1], -1000.0),
194+
mocker.call(inv_ids[2], -1000.0),
195+
mocker.call(inv_ids[3], -1000.0),
196+
]
197+
198+
# After failing 1 inverter, bounds should go down and power shouldn't be
199+
# distributed to that inverter.
200+
await self._fail_pv_inverters([inv_ids[1]], mocks)
201+
await self._recv_reports_until(
202+
bounds_rx,
203+
lambda x: x.bounds is not None and x.bounds.lower.as_watts() == -80000.0,
204+
)
205+
self._assert_report(
206+
await bounds_rx.receive(), power=-4000.0, lower=-80000.0, upper=0.0
207+
)
208+
209+
set_power.reset_mock()
210+
await pv_pool.propose_power(Power.from_watts(-70000.0))
211+
await self._recv_reports_until(
212+
bounds_rx,
213+
lambda x: x.target_power is not None
214+
and x.target_power.as_watts() == -70000.0,
215+
)
216+
217+
self._assert_report(
218+
await bounds_rx.receive(), power=-70000.0, lower=-80000.0, upper=0.0
219+
)
220+
await asyncio.sleep(0.0)
221+
222+
# Components are set initial power
223+
assert set_power.call_count == 3
224+
inv_ids = mocks.microgrid.pv_inverter_ids
225+
assert sorted(set_power.call_args_list, key=lambda x: x.args[0]) == [
226+
mocker.call(inv_ids[0], -10000.0),
227+
mocker.call(inv_ids[2], -30000.0),
228+
mocker.call(inv_ids[3], -30000.0),
229+
]
230+
231+
# After the failed inverter recovers, bounds should go back up and power
232+
# should be distributed to all inverters
233+
await self._fail_pv_inverters([], mocks)
234+
await self._recv_reports_until(
235+
bounds_rx,
236+
lambda x: x.bounds is not None and x.bounds.lower.as_watts() == -100000.0,
237+
)
238+
self._assert_report(
239+
await bounds_rx.receive(), power=-70000.0, lower=-100000.0, upper=0.0
240+
)
241+
242+
set_power.reset_mock()
243+
await pv_pool.propose_power(Power.from_watts(-90000.0))
244+
await self._recv_reports_until(
245+
bounds_rx,
246+
lambda x: x.target_power is not None
247+
and x.target_power.as_watts() == -90000.0,
248+
)
249+
250+
self._assert_report(
251+
await bounds_rx.receive(), power=-90000.0, lower=-100000.0, upper=0.0
252+
)
253+
await asyncio.sleep(0.0)
254+
255+
assert set_power.call_count == 4
256+
inv_ids = mocks.microgrid.pv_inverter_ids
257+
assert sorted(set_power.call_args_list, key=lambda x: x.args[0]) == [
258+
mocker.call(inv_ids[0], -10000.0),
259+
mocker.call(inv_ids[1], -20000.0),
260+
mocker.call(inv_ids[2], -30000.0),
261+
mocker.call(inv_ids[3], -30000.0),
262+
]

0 commit comments

Comments
 (0)