Skip to content

Commit 226d628

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 014f34f commit 226d628

File tree

2 files changed

+109
-12
lines changed

2 files changed

+109
-12
lines changed

tests/timeseries/_battery_pool/test_battery_pool.py

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