@@ -155,6 +155,103 @@ async def test_power_distributor_one_user(self, mocker: MockerFixture) -> None:
155155 assert result .excess_power == approx (200.0 )
156156 assert result .request == request
157157
158+ async def test_power_distributor_exclusion_bounds (
159+ self , mocker : MockerFixture
160+ ) -> None :
161+ """Test if power distributing actor rejects non-zero requests in exclusion bounds."""
162+ mockgrid = MockMicrogrid (grid_meter = False )
163+ mockgrid .add_batteries (2 )
164+ await mockgrid .start (mocker )
165+ await self .init_component_data (mockgrid )
166+
167+ await mockgrid .mock_client .send (
168+ battery_msg (
169+ 9 ,
170+ soc = Metric (60 , Bound (20 , 80 )),
171+ capacity = Metric (98000 ),
172+ power = PowerBounds (- 1000 , - 300 , 300 , 1000 ),
173+ )
174+ )
175+ await mockgrid .mock_client .send (
176+ battery_msg (
177+ 19 ,
178+ soc = Metric (60 , Bound (20 , 80 )),
179+ capacity = Metric (98000 ),
180+ power = PowerBounds (- 1000 , - 300 , 300 , 1000 ),
181+ )
182+ )
183+
184+ channel = Broadcast [Request ]("power_distributor" )
185+ channel_registry = ChannelRegistry (name = "power_distributor" )
186+
187+ attrs = {
188+ "get_working_batteries.return_value" : microgrid .battery_pool ().battery_ids
189+ }
190+ mocker .patch (
191+ "frequenz.sdk.actor.power_distributing.power_distributing.BatteryPoolStatus" ,
192+ return_value = MagicMock (spec = BatteryPoolStatus , ** attrs ),
193+ )
194+
195+ mocker .patch ("asyncio.sleep" , new_callable = AsyncMock )
196+ battery_status_channel = Broadcast [BatteryStatus ]("battery_status" )
197+ distributor = PowerDistributingActor (
198+ requests_receiver = channel .new_receiver (),
199+ channel_registry = channel_registry ,
200+ battery_status_sender = battery_status_channel .new_sender (),
201+ )
202+
203+ ## zero power requests should pass through despite the exclusion bounds.
204+ request = Request (
205+ namespace = self ._namespace ,
206+ power = 0.0 ,
207+ batteries = {9 , 19 },
208+ request_timeout_sec = SAFETY_TIMEOUT ,
209+ )
210+
211+ await channel .new_sender ().send (request )
212+ result_rx = channel_registry .new_receiver (self ._namespace )
213+
214+ done , pending = await asyncio .wait (
215+ [asyncio .create_task (result_rx .receive ())],
216+ timeout = SAFETY_TIMEOUT ,
217+ )
218+
219+ assert len (pending ) == 0
220+ assert len (done ) == 1
221+
222+ result : Result = done .pop ().result ()
223+ assert isinstance (result , Success )
224+ assert result .succeeded_power == approx (0.0 )
225+ assert result .excess_power == approx (0.0 )
226+ assert result .request == request
227+
228+ ## non-zero power requests that fall within the exclusion bounds should be
229+ ## rejected.
230+ request = Request (
231+ namespace = self ._namespace ,
232+ power = 300.0 ,
233+ batteries = {9 , 19 },
234+ request_timeout_sec = SAFETY_TIMEOUT ,
235+ )
236+
237+ await channel .new_sender ().send (request )
238+ result_rx = channel_registry .new_receiver (self ._namespace )
239+
240+ done , pending = await asyncio .wait (
241+ [asyncio .create_task (result_rx .receive ())],
242+ timeout = SAFETY_TIMEOUT ,
243+ )
244+
245+ assert len (pending ) == 0
246+ assert len (done ) == 1
247+
248+ result = done .pop ().result ()
249+ assert isinstance (result , OutOfBounds )
250+ assert result .bounds == PowerBounds (- 1000 , - 600 , 600 , 1000 )
251+ assert result .request == request
252+
253+ await distributor ._stop_actor ()
254+
158255 async def test_battery_soc_nan (self , mocker : MockerFixture ) -> None :
159256 """Test if battery with SoC==NaN is not used."""
160257 mockgrid = MockMicrogrid (grid_meter = False )
0 commit comments