|
6 | 6 | # pylint: disable=too-many-lines |
7 | 7 |
|
8 | 8 |
|
9 | | -from contextlib import AsyncExitStack |
10 | 9 | import asyncio |
11 | 10 | import dataclasses |
12 | 11 | import logging |
13 | 12 | import math |
14 | 13 | from collections.abc import AsyncIterator |
| 14 | +from contextlib import AsyncExitStack |
15 | 15 | from dataclasses import dataclass, is_dataclass, replace |
16 | 16 | from datetime import datetime, timedelta, timezone |
17 | 17 | from typing import Any, Generic, TypeVar |
@@ -590,6 +590,96 @@ async def test_batter_pool_power_two_batteries_per_inverter( |
590 | 590 | assert (await power_receiver.receive()).value == Power.from_watts(-2.0) |
591 | 591 |
|
592 | 592 |
|
| 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 | + |
593 | 683 | async def test_batter_pool_power_no_batteries(mocker: MockerFixture) -> None: |
594 | 684 | """Test power method with no batteries.""" |
595 | 685 | mockgrid = MockMicrogrid( |
|
0 commit comments