Skip to content

Commit

Permalink
Add tests for BatteryPowerFormula fallback
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
ela-kotulska-frequenz committed Aug 23, 2024
1 parent c336633 commit b5bfa34
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 12 deletions.
92 changes: 91 additions & 1 deletion tests/timeseries/_battery_pool/test_battery_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
# pylint: disable=too-many-lines


from contextlib import AsyncExitStack
import asyncio
import dataclasses
import logging
import math
from collections.abc import AsyncIterator
from contextlib import AsyncExitStack
from dataclasses import dataclass, is_dataclass, replace
from datetime import datetime, timedelta, timezone
from typing import Any, Generic, TypeVar
Expand Down Expand Up @@ -590,6 +590,96 @@ async def test_batter_pool_power_two_batteries_per_inverter(
assert (await power_receiver.receive()).value == Power.from_watts(-2.0)


async def test_batter_pool_power_fallback_formula(
mocker: MockerFixture,
) -> None:
"""Test power method with two batteries per inverter."""
gen = GraphGenerator()
mockgrid = MockMicrogrid(
graph=gen.to_graph(
[
ComponentCategory.METER, # Grid meter - shouldn't be included in formula
(
ComponentCategory.METER, # meter with 2 inverters
[
(
ComponentCategory.INVERTER,
[ComponentCategory.BATTERY],
),
(
ComponentCategory.INVERTER,
[ComponentCategory.BATTERY, ComponentCategory.BATTERY],
),
],
),
( # inverter without meter
ComponentCategory.INVERTER,
[ComponentCategory.BATTERY, ComponentCategory.BATTERY],
),
]
),
mocker=mocker,
)

async with mockgrid, AsyncExitStack() as stack:
battery_pool = microgrid.new_battery_pool(priority=5)
stack.push_async_callback(battery_pool.stop)
power_receiver = battery_pool.power.new_receiver()

# Note: BatteryPowerFormula has a "nones-are-zero" rule, that says:
# * if the meter value is None, it should be treated as None.
# * for other components None is treated as 0.

# fmt: off
expected_input_output: list[
tuple[list[float | None], list[float | None], Power | None]
] = [
# ([grid_meter, bat_inv_meter], [bat_inv1, bat_inv2, bat_inv3], expected_power)
# bat_inv_meter is connected to bat_inv1 and bat_inv2
# bat_inv3 has no meter
# Case 1: All components are available, add power form bat_inv_meter and bat_inv3
([-1.0, 2.0], [-100.0, -200.0, -300.0], Power.from_watts(-298.0)),
([-1.0, -10.0], [None, None, -300.0], Power.from_watts(-310.0)),
# Case 2: Meter is unavailable (None).
# Subscribe to the fallback inverters, but return None as the result,
# according to the "nones-are-zero" rule
# Next call should add power from inverters
([-1.0, None], [100.0, 100.0, -300.0], None),
([-1.0, None], [100.0, 100.0, -300.0], Power.from_watts(-100.0)),
# Case 3: bat_inv_3 is unavailable (None). Return 0 from failing component
([-1.0, None], [100.0, 100.0, None], Power.from_watts(200.0)),
# Case 4: bat_inv_meter is available, ignore fallback inverters
([-1.0, 10], [100.0, 100.0, None], Power.from_watts(10.0)),
# Case 4: all components are unavailable (None). Return 0 according to the
# "nones-are-zero" rule.
([-1.0, None], [None, None, None], Power.from_watts(0.0)),
# Case 5: Components becomes available
([-1.0, None], [None, None, 100.0], Power.from_watts(100.0)),
([-1.0, None], [None, 50.0, 100.0], Power.from_watts(150.0)),
([-1.0, None], [-20, 50.0, 100.0], Power.from_watts(130.0)),
([-1.0, -200], [-20, 50.0, 100.0], Power.from_watts(-100.0)),
]
# fmt: on

for idx, (
meter_power,
bat_inv_power,
expected_power,
) in enumerate(expected_input_output):
await mockgrid.mock_resampler.send_meter_power(meter_power)
await mockgrid.mock_resampler.send_bat_inverter_power(bat_inv_power)
mockgrid.mock_resampler.next_ts()

result = await asyncio.wait_for(power_receiver.receive(), timeout=1)
assert result.value == expected_power, (
f"Test case {idx} failed:"
+ f" meter_power: {meter_power}"
+ f" bat_inv_power {bat_inv_power}"
+ f" expected_power: {expected_power}"
+ f" actual_power: {result.value}"
)


async def test_batter_pool_power_no_batteries(mocker: MockerFixture) -> None:
"""Test power method with no batteries."""
mockgrid = MockMicrogrid(
Expand Down
26 changes: 15 additions & 11 deletions tests/timeseries/mock_microgrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,24 +112,28 @@ def __init__( # pylint: disable=too-many-arguments
def filter_comp(category: ComponentCategory) -> list[int]:
if graph is None:
return []
return list(
map(
lambda c: c.component_id,
graph.components(component_categories={category}),
return sorted(
list(
map(
lambda c: c.component_id,
graph.components(component_categories={category}),
)
)
)

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

return [
c.component_id
for c in graph.components(
component_categories={ComponentCategory.INVERTER}
)
if c.type == comp_type
]
return sorted(
[
c.component_id
for c in graph.components(
component_categories={ComponentCategory.INVERTER}
)
if c.type == comp_type
]
)

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

0 comments on commit b5bfa34

Please sign in to comment.