Skip to content

Commit db41689

Browse files
Add tests for BatteryPowerFormula fallback
Fix mockMicrogrid: create senders always in the same order. Previous solution was working because we had simple graphs. Signed-off-by: Elzbieta Kotulska <[email protected]>
1 parent 810ef62 commit db41689

File tree

2 files changed

+106
-12
lines changed

2 files changed

+106
-12
lines changed

tests/timeseries/_battery_pool/test_battery_pool.py

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
# pylint: disable=too-many-lines
77

88

9-
from contextlib import AsyncExitStack
109
import asyncio
1110
import dataclasses
1211
import logging
1312
import math
1413
from collections.abc import AsyncIterator
14+
from contextlib import AsyncExitStack
1515
from dataclasses import dataclass, is_dataclass, replace
1616
from datetime import datetime, timedelta, timezone
1717
from typing import Any, Generic, TypeVar
@@ -590,6 +590,96 @@ async def test_batter_pool_power_two_batteries_per_inverter(
590590
assert (await power_receiver.receive()).value == Power.from_watts(-2.0)
591591

592592

593+
async def test_batter_pool_power_fallback_formula(
594+
mocker: MockerFixture,
595+
) -> None:
596+
"""Test power method with two batteries per inverter."""
597+
gen = GraphGenerator()
598+
mockgrid = MockMicrogrid(
599+
graph=gen.to_graph(
600+
[
601+
ComponentCategory.METER, # Grid meter - shouldn't be included in formula
602+
(
603+
ComponentCategory.METER, # meter with 2 inverters
604+
[
605+
(
606+
ComponentCategory.INVERTER,
607+
[ComponentCategory.BATTERY],
608+
),
609+
(
610+
ComponentCategory.INVERTER,
611+
[ComponentCategory.BATTERY, ComponentCategory.BATTERY],
612+
),
613+
],
614+
),
615+
( # inverter without meter
616+
ComponentCategory.INVERTER,
617+
[ComponentCategory.BATTERY, ComponentCategory.BATTERY],
618+
),
619+
]
620+
),
621+
mocker=mocker,
622+
)
623+
624+
async with mockgrid, AsyncExitStack() as stack:
625+
battery_pool = microgrid.new_battery_pool(priority=5)
626+
stack.push_async_callback(battery_pool.stop)
627+
power_receiver = battery_pool.power.new_receiver()
628+
629+
# Note: BatteryPowerFormula has a "nones-are-zero" rule, that says:
630+
# * if the meter value is None, it should be treated as None.
631+
# * for other components None is treated as 0.
632+
633+
# fmt: off
634+
expected_input_output: list[
635+
tuple[list[float | None], list[float | None], Power | None]
636+
] = [
637+
# ([grid_meter, bat_inv_meter], [bat_inv1, bat_inv2, bat_inv3], expected_power)
638+
# bat_inv_meter is connected to bat_inv1 and bat_inv2
639+
# bat_inv3 has no meter
640+
# Case 1: All components are available, add power form bat_inv_meter and bat_inv3
641+
([-1.0, 2.0], [-100.0, -200.0, -300.0], Power.from_watts(-298.0)),
642+
([-1.0, -10.0], [None, None, -300.0], Power.from_watts(-310.0)),
643+
# Case 2: Meter is unavailable (None).
644+
# Subscribe to the fallback inverters, but return None as the result,
645+
# according to the "nones-are-zero" rule
646+
# Next call should add power from inverters
647+
([-1.0, None], [100.0, 100.0, -300.0], None),
648+
([-1.0, None], [100.0, 100.0, -300.0], Power.from_watts(-100.0)),
649+
# Case 3: bat_inv_3 is unavailable (None). Return 0 from failing component
650+
([-1.0, None], [100.0, 100.0, None], Power.from_watts(200.0)),
651+
# Case 4: bat_inv_meter is available, ignore fallback inverters
652+
([-1.0, 10], [100.0, 100.0, None], Power.from_watts(10.0)),
653+
# Case 4: all components are unavailable (None). Return 0 according to the
654+
# "nones-are-zero" rule.
655+
([-1.0, None], [None, None, None], Power.from_watts(0.0)),
656+
# Case 5: Components becomes available
657+
([-1.0, None], [None, None, 100.0], Power.from_watts(100.0)),
658+
([-1.0, None], [None, 50.0, 100.0], Power.from_watts(150.0)),
659+
([-1.0, None], [-20, 50.0, 100.0], Power.from_watts(130.0)),
660+
([-1.0, -200], [-20, 50.0, 100.0], Power.from_watts(-100.0)),
661+
]
662+
# fmt: on
663+
664+
for idx, (
665+
meter_power,
666+
bat_inv_power,
667+
expected_power,
668+
) in enumerate(expected_input_output):
669+
await mockgrid.mock_resampler.send_meter_power(meter_power)
670+
await mockgrid.mock_resampler.send_bat_inverter_power(bat_inv_power)
671+
mockgrid.mock_resampler.next_ts()
672+
673+
result = await asyncio.wait_for(power_receiver.receive(), timeout=1)
674+
assert result.value == expected_power, (
675+
f"Test case {idx} failed:"
676+
+ f" meter_power: {meter_power}"
677+
+ f" bat_inv_power {bat_inv_power}"
678+
+ f" expected_power: {expected_power}"
679+
+ f" actual_power: {result.value}"
680+
)
681+
682+
593683
async def test_batter_pool_power_no_batteries(mocker: MockerFixture) -> None:
594684
"""Test power method with no batteries."""
595685
mockgrid = MockMicrogrid(

tests/timeseries/mock_microgrid.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -112,24 +112,28 @@ def __init__( # pylint: disable=too-many-arguments
112112
def filter_comp(category: ComponentCategory) -> list[int]:
113113
if graph is None:
114114
return []
115-
return list(
116-
map(
117-
lambda c: c.component_id,
118-
graph.components(component_categories={category}),
115+
return sorted(
116+
list(
117+
map(
118+
lambda c: c.component_id,
119+
graph.components(component_categories={category}),
120+
)
119121
)
120122
)
121123

122124
def inverters(comp_type: InverterType) -> list[int]:
123125
if graph is None:
124126
return []
125127

126-
return [
127-
c.component_id
128-
for c in graph.components(
129-
component_categories={ComponentCategory.INVERTER}
130-
)
131-
if c.type == comp_type
132-
]
128+
return sorted(
129+
[
130+
c.component_id
131+
for c in graph.components(
132+
component_categories={ComponentCategory.INVERTER}
133+
)
134+
if c.type == comp_type
135+
]
136+
)
133137

134138
self.chp_ids: list[int] = filter_comp(ComponentCategory.CHP)
135139
self.battery_ids: list[int] = filter_comp(ComponentCategory.BATTERY)

0 commit comments

Comments
 (0)