@@ -138,7 +138,7 @@ async def test_login_skips_when_token_valid(aresponses: ResponsesMockServer) ->
138138
139139@pytest .mark .asyncio
140140async def test_get_thermostats (aresponses : ResponsesMockServer ) -> None :
141- """Test fetching thermostats through the buildings->tree->control flow."""
141+ """Test fetching thermostats through the buildings->tree->zone flow."""
142142 _add_login_response (aresponses )
143143 aresponses .add (
144144 "ojmicroline.test.host" ,
@@ -172,12 +172,12 @@ async def test_get_thermostats(aresponses: ResponsesMockServer) -> None:
172172 )
173173 aresponses .add (
174174 "ojmicroline.test.host" ,
175- "/thermostats/2cb3e6c5-8cf8-4e7e-943a-42618c34a506 /control" ,
175+ "/zones/ae85d8fc-1b4a-445a-bdf1-05f970fc3da4 /control/thermostats " ,
176176 "GET" ,
177177 Response (
178178 status = 200 ,
179179 headers = {"Content-Type" : "application/json" },
180- text = load_fixtures ("wg5_thermostat_control .json" ),
180+ text = load_fixtures ("wg5_zone_thermostats .json" ),
181181 ),
182182 )
183183 aresponses .add (
@@ -263,16 +263,16 @@ async def test_get_thermostats_no_schedule_no_readouts(
263263 ),
264264 )
265265 # Control data with no scheduleId
266- control_data = json .loads (load_fixtures ("wg5_thermostat_control .json" ))
267- control_data ["data" ]["scheduleData" ] = {}
266+ zone_data = json .loads (load_fixtures ("wg5_zone_thermostats .json" ))
267+ zone_data ["data" ][ 0 ]["scheduleData" ] = {}
268268 aresponses .add (
269269 "ojmicroline.test.host" ,
270- "/thermostats/2cb3e6c5-8cf8-4e7e-943a-42618c34a506 /control" ,
270+ "/zones/ae85d8fc-1b4a-445a-bdf1-05f970fc3da4 /control/thermostats " ,
271271 "GET" ,
272272 Response (
273273 status = 200 ,
274274 headers = {"Content-Type" : "application/json" },
275- text = json .dumps (control_data ),
275+ text = json .dumps (zone_data ),
276276 ),
277277 )
278278 # Detail data with no readout temperatures
@@ -302,6 +302,134 @@ async def test_get_thermostats_no_schedule_no_readouts(
302302 assert thermostat .temperature_room is None
303303
304304
305+ ZONE_ID = "ae85d8fc-1b4a-445a-bdf1-05f970fc3da4"
306+ BUILDING_ID = "57dc7778-4ed3-4382-9e89-5cf2e0bc0f8a"
307+ SCHEDULE_ID = "f9aac20b-4e45-415b-9f56-e53014ee8639"
308+
309+
310+ def _json_response (data : object ) -> Response :
311+ """Return a JSON response for aresponses."""
312+ return Response (
313+ status = 200 ,
314+ headers = {"Content-Type" : "application/json" },
315+ text = json .dumps (data ),
316+ )
317+
318+
319+ def _add_building_responses (
320+ aresponses : ResponsesMockServer , tree : dict [str , object ]
321+ ) -> None :
322+ """Add the login, buildings, away mode and tree responses."""
323+ _add_login_response (aresponses )
324+ aresponses .add (
325+ "ojmicroline.test.host" ,
326+ "/buildings" ,
327+ "GET" ,
328+ _json_response (json .loads (load_fixtures ("wg5_buildings.json" ))),
329+ )
330+ aresponses .add (
331+ "ojmicroline.test.host" ,
332+ f"/awaymode/{ BUILDING_ID } " ,
333+ "GET" ,
334+ _json_response ({"status" : {"code" : "OK" }}),
335+ )
336+ aresponses .add (
337+ "ojmicroline.test.host" ,
338+ f"/buildings/{ BUILDING_ID } /tree" ,
339+ "GET" ,
340+ _json_response (tree ),
341+ )
342+
343+
344+ def _zone_thermostat (thermostat_id : str ) -> dict [str , object ]:
345+ """Return the zone control data of a thermostat with the given ID."""
346+ data : dict [str , object ] = json .loads (load_fixtures ("wg5_zone_thermostats.json" ))[
347+ "data"
348+ ][0 ]
349+ data ["id" ] = thermostat_id
350+ return data
351+
352+
353+ @pytest .mark .asyncio
354+ async def test_get_thermostats_zone_pages_and_shared_schedule (
355+ aresponses : ResponsesMockServer ,
356+ ) -> None :
357+ """Test fetching a zone over multiple pages, with a shared schedule."""
358+ tree = json .loads (load_fixtures ("wg5_building_tree.json" ))
359+ _add_building_responses (aresponses , tree )
360+
361+ page_numbers : list [str ] = []
362+ pages = [["thermostat-1" , "thermostat-2" ], ["thermostat-3" ]]
363+
364+ async def zone_handler (request : web .BaseRequest ) -> Response :
365+ assert request .query ["pageSize" ] == "100"
366+ page_number = int (request .query ["pageNumber" ])
367+ page_numbers .append (request .query ["pageNumber" ])
368+ return _json_response (
369+ {
370+ "page" : {"total" : 3 , "totalPages" : 2 , "size" : 100 },
371+ "data" : [_zone_thermostat (tid ) for tid in pages [page_number ]],
372+ "status" : {"code" : "OK" },
373+ }
374+ )
375+
376+ for _ in pages :
377+ aresponses .add (
378+ "ojmicroline.test.host" ,
379+ f"/zones/{ ZONE_ID } /control/thermostats" ,
380+ "GET" ,
381+ zone_handler ,
382+ )
383+ detail = json .loads (load_fixtures ("wg5_thermostat_detail.json" ))
384+ for thermostat_id in ("thermostat-1" , "thermostat-2" , "thermostat-3" ):
385+ aresponses .add (
386+ "ojmicroline.test.host" ,
387+ f"/thermostats/{ thermostat_id } " ,
388+ "GET" ,
389+ _json_response (detail ),
390+ )
391+ # The thermostats share one schedule, which is only fetched once.
392+ aresponses .add (
393+ "ojmicroline.test.host" ,
394+ f"/schedules/{ SCHEDULE_ID } " ,
395+ "GET" ,
396+ _json_response (json .loads (load_fixtures ("wg5_schedule.json" ))),
397+ )
398+
399+ async with aiohttp .ClientSession () as session :
400+ client = OJMicroline (api = _make_api (), session = session )
401+ thermostats = await client .get_thermostats ()
402+
403+ # Every route was used exactly once and no other requests were made.
404+ aresponses .assert_no_unused_routes ()
405+ aresponses .assert_all_requests_matched ()
406+ assert page_numbers == ["0" , "1" ]
407+ assert [t .serial_number for t in thermostats ] == [
408+ "thermostat-1" ,
409+ "thermostat-2" ,
410+ "thermostat-3" ,
411+ ]
412+ assert all (t .schedule is not None for t in thermostats )
413+ # Every thermostat has its own copy of the shared schedule.
414+ assert thermostats [0 ].schedule is not thermostats [1 ].schedule
415+
416+
417+ @pytest .mark .asyncio
418+ async def test_get_thermostats_skips_empty_zone (
419+ aresponses : ResponsesMockServer ,
420+ ) -> None :
421+ """Test that zones without thermostats are not requested."""
422+ tree = json .loads (load_fixtures ("wg5_building_tree.json" ))
423+ tree ["data" ]["zones" ][0 ]["thermostats" ] = []
424+ _add_building_responses (aresponses , tree )
425+
426+ async with aiohttp .ClientSession () as session :
427+ client = OJMicroline (api = _make_api (), session = session )
428+ assert await client .get_thermostats () == []
429+
430+ aresponses .assert_plan_strictly_followed ()
431+
432+
305433@pytest .mark .asyncio
306434async def test_get_energy_usage_no_building_id () -> None :
307435 """Test energy usage returns empty list when building_id is missing."""
0 commit comments