|
2 | 2 | # Copyright © 2023 Frequenz Energy-as-a-Service GmbH |
3 | 3 |
|
4 | 4 | """Tests for battery pool.""" |
| 5 | + |
5 | 6 | from __future__ import annotations |
6 | 7 |
|
7 | 8 | import asyncio |
|
25 | 26 | from frequenz.sdk.actor import ResamplerConfig |
26 | 27 | from frequenz.sdk.actor.power_distributing import BatteryStatus |
27 | 28 | from frequenz.sdk.microgrid.component import ComponentCategory |
28 | | -from frequenz.sdk.timeseries import Energy, Percentage, Power, Sample |
| 29 | +from frequenz.sdk.timeseries import Energy, Percentage, Power, Sample, Temperature |
29 | 30 | from frequenz.sdk.timeseries.battery_pool import BatteryPool, Bound, PowerMetrics |
30 | 31 | from frequenz.sdk.timeseries.battery_pool._metric_calculator import ( |
31 | 32 | battery_inverter_mapping, |
|
45 | 46 | # pylint doesn't understand fixtures. It thinks it is redefined name. |
46 | 47 | # pylint: disable=redefined-outer-name |
47 | 48 |
|
| 49 | +# pylint: disable=too-many-lines |
| 50 | + |
48 | 51 |
|
49 | 52 | @pytest.fixture() |
50 | 53 | def event_loop() -> Iterator[async_solipsism.EventLoop]: |
@@ -337,6 +340,24 @@ async def test_battery_pool_power_bounds(setup_batteries_pool: SetupArgs) -> Non |
337 | 340 | await run_power_bounds_test(setup_batteries_pool) |
338 | 341 |
|
339 | 342 |
|
| 343 | +async def test_all_batteries_temperature(setup_all_batteries: SetupArgs) -> None: |
| 344 | + """Test temperature for battery pool with all components in the microgrid. |
| 345 | +
|
| 346 | + Args: |
| 347 | + setup_all_batteries: Fixture that creates needed microgrid tools. |
| 348 | + """ |
| 349 | + await run_temperature_test(setup_all_batteries) |
| 350 | + |
| 351 | + |
| 352 | +async def test_battery_pool_temperature(setup_batteries_pool: SetupArgs) -> None: |
| 353 | + """Test temperature for battery pool with subset of components in the microgrid. |
| 354 | +
|
| 355 | + Args: |
| 356 | + setup_all_batteries: Fixture that creates needed microgrid tools. |
| 357 | + """ |
| 358 | + await run_temperature_test(setup_batteries_pool) |
| 359 | + |
| 360 | + |
340 | 361 | def assert_dataclass(arg: Any) -> None: |
341 | 362 | """Raise assert error if argument is not dataclass. |
342 | 363 |
|
@@ -981,3 +1002,112 @@ async def run_power_bounds_test( # pylint: disable=too-many-locals |
981 | 1002 | streamer.start_streaming(latest_data, sampling_rate=0.1) |
982 | 1003 | msg = await asyncio.wait_for(receiver.receive(), timeout=waiting_time_sec) |
983 | 1004 | compare_messages(msg, PowerMetrics(now, Bound(-100, 0), Bound(0, 100)), 0.2) |
| 1005 | + |
| 1006 | + |
| 1007 | +async def run_temperature_test( # pylint: disable=too-many-locals |
| 1008 | + setup_args: SetupArgs, |
| 1009 | +) -> None: |
| 1010 | + """Test if temperature metric is working as expected.""" |
| 1011 | + battery_pool = setup_args.battery_pool |
| 1012 | + mock_microgrid = setup_args.mock_microgrid |
| 1013 | + streamer = setup_args.streamer |
| 1014 | + battery_status_sender = setup_args.battery_status_sender |
| 1015 | + |
| 1016 | + all_batteries = get_components(mock_microgrid, ComponentCategory.BATTERY) |
| 1017 | + await battery_status_sender.send( |
| 1018 | + BatteryStatus(working=all_batteries, uncertain=set()) |
| 1019 | + ) |
| 1020 | + bat_inv_map = battery_inverter_mapping(all_batteries) |
| 1021 | + |
| 1022 | + for battery_id, inverter_id in bat_inv_map.items(): |
| 1023 | + # Sampling rate choose to reflect real application. |
| 1024 | + streamer.start_streaming( |
| 1025 | + BatteryDataWrapper( |
| 1026 | + component_id=battery_id, |
| 1027 | + timestamp=datetime.now(tz=timezone.utc), |
| 1028 | + temperature=25.0, |
| 1029 | + ), |
| 1030 | + sampling_rate=0.05, |
| 1031 | + ) |
| 1032 | + streamer.start_streaming( |
| 1033 | + InverterDataWrapper( |
| 1034 | + component_id=inverter_id, |
| 1035 | + timestamp=datetime.now(tz=timezone.utc), |
| 1036 | + ), |
| 1037 | + sampling_rate=0.1, |
| 1038 | + ) |
| 1039 | + |
| 1040 | + receiver = battery_pool.temperature.new_receiver() |
| 1041 | + |
| 1042 | + msg = await asyncio.wait_for( |
| 1043 | + receiver.receive(), timeout=WAIT_FOR_COMPONENT_DATA_SEC + 0.2 |
| 1044 | + ) |
| 1045 | + now = datetime.now(tz=timezone.utc) |
| 1046 | + expected = Sample(now, value=Temperature.from_celsius(25.0)) |
| 1047 | + compare_messages(msg, expected, WAIT_FOR_COMPONENT_DATA_SEC + 0.2) |
| 1048 | + |
| 1049 | + batteries_in_pool = list(battery_pool.battery_ids) |
| 1050 | + bat_0, bat_1 = batteries_in_pool |
| 1051 | + scenarios: list[Scenario[Sample[Temperature]]] = [ |
| 1052 | + Scenario( |
| 1053 | + bat_0, |
| 1054 | + {"temperature": 30.0}, |
| 1055 | + Sample(now, value=Temperature.from_celsius(27.5)), |
| 1056 | + ), |
| 1057 | + Scenario( |
| 1058 | + bat_1, |
| 1059 | + {"temperature": 20.0}, |
| 1060 | + Sample(now, value=Temperature.from_celsius(25.0)), |
| 1061 | + ), |
| 1062 | + Scenario( |
| 1063 | + bat_0, |
| 1064 | + {"temperature": math.nan}, |
| 1065 | + Sample(now, value=Temperature.from_celsius(20.0)), |
| 1066 | + ), |
| 1067 | + Scenario( |
| 1068 | + bat_1, |
| 1069 | + {"temperature": math.nan}, |
| 1070 | + None, |
| 1071 | + ), |
| 1072 | + Scenario( |
| 1073 | + bat_0, |
| 1074 | + {"temperature": 30.0}, |
| 1075 | + Sample(now, value=Temperature.from_celsius(30.0)), |
| 1076 | + ), |
| 1077 | + Scenario( |
| 1078 | + bat_1, |
| 1079 | + {"temperature": 15.0}, |
| 1080 | + Sample(now, value=Temperature.from_celsius(22.5)), |
| 1081 | + ), |
| 1082 | + ] |
| 1083 | + |
| 1084 | + waiting_time_sec = setup_args.min_update_interval + 0.02 |
| 1085 | + await run_scenarios(scenarios, streamer, receiver, waiting_time_sec) |
| 1086 | + |
| 1087 | + await run_test_battery_status_channel( |
| 1088 | + battery_status_sender=battery_status_sender, |
| 1089 | + battery_pool_metric_receiver=receiver, |
| 1090 | + all_batteries=all_batteries, |
| 1091 | + batteries_in_pool=batteries_in_pool, |
| 1092 | + waiting_time_sec=waiting_time_sec, |
| 1093 | + all_pool_result=Sample(now, Temperature.from_celsius(22.5)), |
| 1094 | + only_first_battery_result=Sample(now, Temperature.from_celsius(30.0)), |
| 1095 | + ) |
| 1096 | + |
| 1097 | + # one battery stops sending data. |
| 1098 | + await streamer.stop_streaming(bat_1) |
| 1099 | + await asyncio.sleep(MAX_BATTERY_DATA_AGE_SEC + 0.2) |
| 1100 | + msg = await asyncio.wait_for(receiver.receive(), timeout=waiting_time_sec) |
| 1101 | + compare_messages(msg, Sample(now, Temperature.from_celsius(30.0)), 0.2) |
| 1102 | + |
| 1103 | + # All batteries stopped sending data. |
| 1104 | + await streamer.stop_streaming(bat_0) |
| 1105 | + await asyncio.sleep(MAX_BATTERY_DATA_AGE_SEC + 0.2) |
| 1106 | + msg = await asyncio.wait_for(receiver.receive(), timeout=waiting_time_sec) |
| 1107 | + assert msg is None |
| 1108 | + |
| 1109 | + # one battery started sending data. |
| 1110 | + latest_data = streamer.get_current_component_data(bat_1) |
| 1111 | + streamer.start_streaming(latest_data, sampling_rate=0.1) |
| 1112 | + msg = await asyncio.wait_for(receiver.receive(), timeout=waiting_time_sec) |
| 1113 | + compare_messages(msg, Sample(now, Temperature.from_celsius(15.0)), 0.2) |
0 commit comments