@@ -60,6 +60,7 @@ async def test_producer_power_no_pv_no_consumer_meter(
6060 producer_power_receiver = producer .power .new_receiver ()
6161
6262 await mockgrid .mock_resampler .send_chp_power ([2.0 ])
63+
6364 assert (await producer_power_receiver .receive ()).value == Power .from_watts (
6465 2.0
6566 )
@@ -94,3 +95,83 @@ async def test_no_producer_power(self, mocker: MockerFixture) -> None:
9495 assert (await producer_power_receiver .receive ()).value == Power .from_watts (
9596 0.0
9697 )
98+
99+ async def test_producer_fallback_formula (self , mocker : MockerFixture ) -> None :
100+ """Test the producer power formula with fallback formulas."""
101+ mockgrid = MockMicrogrid (grid_meter = False , mocker = mocker )
102+ mockgrid .add_solar_inverters (2 )
103+ # CHP has no meter, so no fallback component
104+ mockgrid .add_chps (1 , no_meters = True )
105+
106+ async with mockgrid , AsyncExitStack () as stack :
107+ producer = microgrid .producer ()
108+ stack .push_async_callback (producer .stop )
109+ producer_power_receiver = producer .power .new_receiver ()
110+
111+ # Note: ProducerPowerFormula has a "nones-are-zero" rule, that says:
112+ # * if the meter value is None, it should be treated as None.
113+ # * for other components None is treated as 0.
114+
115+ # fmt: off
116+ expected_input_output : list [
117+ tuple [list [float | None ], list [float | None ], list [float | None ], Power | None ]
118+ ] = [
119+ # ([pv_meter_power], [pv_inverter_power], [chp_power], expected_power)
120+ # Add power from meters and chp
121+ ([- 1.0 , - 2.0 ], [None , - 200.0 ], [300 ], Power .from_watts (297.0 )),
122+ ([- 1.0 , - 10 ], [- 100.0 , - 200.0 ], [400 ], Power .from_watts (389.0 )),
123+ # Case 2: The first meter is unavailable (None).
124+ # Subscribe to the fallback inverter, but return None as the result,
125+ # according to the "nones-are-zero" rule
126+ ([None , - 2.0 ], [- 100 , - 200.0 ], [400 ], None ),
127+ # Case 3: First meter is unavailable (None). Fallback inverter provides
128+ # a value.
129+ # Add second meter, first inverter and chp power
130+ ([None , - 2.0 ], [- 100 , - 200.0 ], [400 ], Power .from_watts (298.0 )),
131+ ([None , - 2.0 ], [- 50 , - 200.0 ], [300 ], Power .from_watts (248.0 )),
132+ # Case 4: Both first meter and its fallback inverter are unavailable
133+ # (None). Return 0 from failing component according to the
134+ # "nones-are-zero" rule.
135+ ([None , - 2.0 ], [None , - 200.0 ], [300 ], Power .from_watts (298.0 )),
136+ ([None , - 10.0 ], [- 20.0 , - 200.0 ], [300 ], Power .from_watts (270.0 )),
137+ # Case 5: CHP is unavailable. Return 0 from failing component
138+ # according to the "nones-are-zero" rule.
139+ ([None , - 10.0 ], [- 20.0 , - 200.0 ], [None ], Power .from_watts (- 30.0 )),
140+ # Case 6: Both meters are unavailable (None). Subscribe for fallback inverter
141+ ([None , None ], [- 20.0 , - 200.0 ], [None ], None ),
142+ ([None , None ], [- 20.0 , - 200.0 ], [None ], Power .from_watts (- 220.0 )),
143+ ([None , None ], [None , - 200.0 ], [None ], Power .from_watts (- 200.0 )),
144+ # Case 7: All components are unavailable (None). Return 0 according to the
145+ # "nones-are-zero" rule.
146+ ([None , None ], [None , None ], [None ], Power .from_watts (0 )),
147+ ([None , None ], [None , None ], [None ], Power .from_watts (0 )),
148+ ([None , None ], [None , None ], [300.0 ], Power .from_watts (300.0 )),
149+ ([- 200.0 , None ], [None , - 100.0 ], [50.0 ], Power .from_watts (- 250.0 )),
150+ ([- 200.0 , - 200.0 ], [- 10.0 , - 20.0 ], [50.0 ], Power .from_watts (- 350.0 )),
151+ # Case 8: Meter is unavailable but we already subscribed for inverter
152+ # So don't return None in this case. Just proper formula result.
153+ ([None , - 200.0 ], [- 10.0 , - 100.0 ], [50.0 ], Power .from_watts (- 160.0 )),
154+
155+ ]
156+ # fmt: on
157+
158+ for idx , (
159+ meter_power ,
160+ pv_inverter_power ,
161+ chp_power ,
162+ expected_power ,
163+ ) in enumerate (expected_input_output ):
164+ await mockgrid .mock_resampler .send_chp_power (chp_power )
165+ await mockgrid .mock_resampler .send_meter_power (meter_power )
166+ await mockgrid .mock_resampler .send_pv_inverter_power (pv_inverter_power )
167+ mockgrid .mock_resampler .next_ts ()
168+
169+ result = await producer_power_receiver .receive ()
170+ assert result .value == expected_power , (
171+ f"Test case { idx } failed:"
172+ + f" meter_power: { meter_power } "
173+ + f" pv_inverter_power { pv_inverter_power } "
174+ + f" chp_power { chp_power } "
175+ + f" expected_power: { expected_power } "
176+ + f" actual_power: { result .value } "
177+ )
0 commit comments