66
77import pytest
88
9- from roborock .data .containers import CombinedMapInfo , NamedRoomMapping
9+ from roborock .data .containers import CombinedMapInfo , HomeDataRoom , NamedRoomMapping
1010from roborock .data .v1 .v1_code_mappings import RoborockStateCode
1111from roborock .data .v1 .v1_containers import MultiMapsListMapInfo , MultiMapsListRoom
1212from roborock .devices .cache import DeviceCache , DeviceCacheData , InMemoryCache
@@ -139,16 +139,25 @@ def rooms_trait(device: RoborockDevice) -> RoomsTrait:
139139 return device .v1_properties .rooms
140140
141141
142+ @pytest .fixture
143+ def mock_web_api () -> AsyncMock :
144+ """Create a mock web API client."""
145+ mock = AsyncMock ()
146+ mock .get_rooms .return_value = []
147+ return mock
148+
149+
142150@pytest .fixture
143151def home_trait (
144152 status_trait : StatusTrait ,
145153 maps_trait : MapsTrait ,
146154 map_content_trait : MapContentTrait ,
147155 rooms_trait : RoomsTrait ,
148156 device_cache : DeviceCache ,
157+ mock_web_api : AsyncMock ,
149158) -> HomeTrait :
150159 """Create a HomeTrait instance with mocked dependencies."""
151- return HomeTrait (status_trait , maps_trait , map_content_trait , rooms_trait , device_cache )
160+ return HomeTrait (status_trait , maps_trait , map_content_trait , rooms_trait , device_cache , mock_web_api )
152161
153162
154163@pytest .fixture (autouse = True )
@@ -227,7 +236,7 @@ async def test_discover_home_empty_cache(
227236 assert map_123_data .rooms [0 ].segment_id == 18
228237 assert map_123_data .rooms [0 ].name == "Example room 3"
229238 assert map_123_data .rooms [1 ].segment_id == 19
230- assert map_123_data .rooms [1 ].name == "Unknown " # Not in mock home data
239+ assert map_123_data .rooms [1 ].name == "Map 123 " # Not in mock home data
231240
232241 map_123_content = home_trait .home_map_content [123 ]
233242 assert map_123_content is not None
@@ -620,6 +629,7 @@ async def test_single_map_no_switching(
620629async def test_refresh_map_info_room_override_and_addition_logic (
621630 home_trait : HomeTrait ,
622631 rooms_trait : RoomsTrait ,
632+ mock_web_api : AsyncMock ,
623633) -> None :
624634 """Test the room override and addition logic in _refresh_map_info.
625635
@@ -660,6 +670,9 @@ async def test_refresh_map_info_room_override_and_addition_logic(
660670 NamedRoomMapping (segment_id = 18 , iot_id = "2362041" , name = "Example room 3" ), # Not in map_info, should be added
661671 ]
662672
673+ # Mock web API to return empty rooms (no resolution)
674+ mock_web_api .get_rooms .return_value = []
675+
663676 # Mock rooms_trait.refresh to prevent actual device calls
664677 with patch .object (rooms_trait , "refresh" , new_callable = AsyncMock ):
665678 result = await home_trait ._refresh_map_info (map_info )
@@ -676,9 +689,9 @@ async def test_refresh_map_info_room_override_and_addition_logic(
676689 assert sorted_rooms [0 ].name == "Kitchen from map_info"
677690 assert sorted_rooms [0 ].iot_id == "2362048"
678691
679- # Room 17: from rooms_trait with "Unknown", added because not in map_info
692+ # Room 17: from rooms_trait with "Unknown", falls back to "Map 0"
680693 assert sorted_rooms [1 ].segment_id == 17
681- assert sorted_rooms [1 ].name == "Unknown "
694+ assert sorted_rooms [1 ].name == "Map 0 "
682695 assert sorted_rooms [1 ].iot_id == "2362044"
683696
684697 # Room 18: from rooms_trait with valid name, added because not in map_info
@@ -690,3 +703,163 @@ async def test_refresh_map_info_room_override_and_addition_logic(
690703 assert sorted_rooms [3 ].segment_id == 19
691704 assert sorted_rooms [3 ].name == "Updated Bedroom Name"
692705 assert sorted_rooms [3 ].iot_id == "2362042"
706+
707+
708+ async def test_get_rooms_resolves_unknown_rooms (
709+ home_trait : HomeTrait ,
710+ rooms_trait : RoomsTrait ,
711+ mock_web_api : AsyncMock ,
712+ ) -> None :
713+ """Test that get_rooms from web API resolves unknown room names."""
714+ map_info = MultiMapsListMapInfo (
715+ map_flag = 0 ,
716+ name = "Test Map" ,
717+ rooms = [
718+ MultiMapsListRoom (id = 16 , iot_name_id = "2362048" , iot_name = None ),
719+ MultiMapsListRoom (id = 17 , iot_name_id = "2362044" , iot_name = None ),
720+ ],
721+ )
722+
723+ rooms_trait .rooms = [
724+ NamedRoomMapping (segment_id = 16 , iot_id = "2362048" , name = "Unknown" ),
725+ NamedRoomMapping (segment_id = 17 , iot_id = "2362044" , name = "Unknown" ),
726+ ]
727+
728+ # Web API returns fresh room names
729+ mock_web_api .get_rooms .return_value = [
730+ HomeDataRoom (id = 2362048 , name = "Living Room" ),
731+ HomeDataRoom (id = 2362044 , name = "Kitchen" ),
732+ ]
733+
734+ with patch .object (rooms_trait , "refresh" , new_callable = AsyncMock ):
735+ result = await home_trait ._refresh_map_info (map_info )
736+
737+ sorted_rooms = sorted (result .rooms , key = lambda r : r .segment_id )
738+ assert sorted_rooms [0 ].name == "Living Room"
739+ assert sorted_rooms [1 ].name == "Kitchen"
740+ mock_web_api .get_rooms .assert_called_once ()
741+
742+
743+ async def test_get_rooms_called_once_for_same_unknown_room (
744+ home_trait : HomeTrait ,
745+ rooms_trait : RoomsTrait ,
746+ mock_web_api : AsyncMock ,
747+ ) -> None :
748+ """Test that get_rooms is not re-called for an already-seen unknown room."""
749+ map_info = MultiMapsListMapInfo (
750+ map_flag = 42 ,
751+ name = "Test Map" ,
752+ rooms = [
753+ MultiMapsListRoom (id = 16 , iot_name_id = "2362048" , iot_name = None ),
754+ ],
755+ )
756+
757+ rooms_trait .rooms = [
758+ NamedRoomMapping (segment_id = 16 , iot_id = "2362048" , name = "Unknown" ),
759+ ]
760+
761+ # Web API returns empty (no resolution)
762+ mock_web_api .get_rooms .return_value = []
763+
764+ with patch .object (rooms_trait , "refresh" , new_callable = AsyncMock ):
765+ result1 = await home_trait ._refresh_map_info (map_info )
766+ result2 = await home_trait ._refresh_map_info (map_info )
767+
768+ # Both calls should fall back to "Map 42"
769+ assert result1 .rooms [0 ].name == "Map 42"
770+ assert result2 .rooms [0 ].name == "Map 42"
771+ # get_rooms should only be called once for the same unknown room
772+ mock_web_api .get_rooms .assert_called_once ()
773+
774+
775+ async def test_get_rooms_called_again_for_new_unknown_room (
776+ home_trait : HomeTrait ,
777+ rooms_trait : RoomsTrait ,
778+ mock_web_api : AsyncMock ,
779+ ) -> None :
780+ """Test that get_rooms is called again when a new unknown room appears."""
781+ map_info = MultiMapsListMapInfo (
782+ map_flag = 42 ,
783+ name = "Test Map" ,
784+ rooms = [
785+ MultiMapsListRoom (id = 16 , iot_name_id = "2362048" , iot_name = None ),
786+ ],
787+ )
788+
789+ rooms_trait .rooms = [
790+ NamedRoomMapping (segment_id = 16 , iot_id = "2362048" , name = "Unknown" ),
791+ ]
792+
793+ # Web API returns empty (no resolution)
794+ mock_web_api .get_rooms .return_value = []
795+
796+ with patch .object (rooms_trait , "refresh" , new_callable = AsyncMock ):
797+ result1 = await home_trait ._refresh_map_info (map_info )
798+
799+ # Add a brand-new unknown room for the same map
800+ rooms_trait .rooms = [
801+ NamedRoomMapping (segment_id = 16 , iot_id = "2362048" , name = "Unknown" ),
802+ NamedRoomMapping (segment_id = 17 , iot_id = "2362044" , name = "Unknown" ),
803+ ]
804+ result2 = await home_trait ._refresh_map_info (map_info )
805+
806+ assert sorted (room .name for room in result1 .rooms ) == ["Map 42" ]
807+ assert sorted (room .name for room in result2 .rooms ) == ["Map 42" , "Map 42" ]
808+ assert mock_web_api .get_rooms .call_count == 2
809+
810+
811+ async def test_get_rooms_called_again_for_new_unknown_iot_id_same_segment (
812+ home_trait : HomeTrait ,
813+ rooms_trait : RoomsTrait ,
814+ mock_web_api : AsyncMock ,
815+ ) -> None :
816+ """Test that get_rooms is called again for a new unknown iot_id on the same segment."""
817+ map_info = MultiMapsListMapInfo (
818+ map_flag = 42 ,
819+ name = "Test Map" ,
820+ )
821+
822+ rooms_trait .rooms = [
823+ NamedRoomMapping (segment_id = 16 , iot_id = "2362048" , name = "Unknown" ),
824+ ]
825+
826+ mock_web_api .get_rooms .return_value = []
827+
828+ with patch .object (rooms_trait , "refresh" , new_callable = AsyncMock ):
829+ await home_trait ._refresh_map_info (map_info )
830+
831+ # Same segment, but now with a different iot_id.
832+ rooms_trait .rooms = [
833+ NamedRoomMapping (segment_id = 16 , iot_id = "2362999" , name = "Unknown" ),
834+ ]
835+ await home_trait ._refresh_map_info (map_info )
836+
837+ assert mock_web_api .get_rooms .call_count == 2
838+
839+
840+ async def test_get_rooms_failure_falls_back_to_map_flag (
841+ home_trait : HomeTrait ,
842+ rooms_trait : RoomsTrait ,
843+ mock_web_api : AsyncMock ,
844+ ) -> None :
845+ """Test that get_rooms failure gracefully falls back to 'Map {flag}'."""
846+ map_info = MultiMapsListMapInfo (
847+ map_flag = 7 ,
848+ name = "Test Map" ,
849+ rooms = [
850+ MultiMapsListRoom (id = 16 , iot_name_id = "2362048" , iot_name = None ),
851+ ],
852+ )
853+
854+ rooms_trait .rooms = [
855+ NamedRoomMapping (segment_id = 16 , iot_id = "2362048" , name = "Unknown" ),
856+ ]
857+
858+ # Web API raises an exception
859+ mock_web_api .get_rooms .side_effect = Exception ("API error" )
860+
861+ with patch .object (rooms_trait , "refresh" , new_callable = AsyncMock ):
862+ result = await home_trait ._refresh_map_info (map_info )
863+
864+ assert result .rooms [0 ].name == "Map 7"
865+ mock_web_api .get_rooms .assert_called_once ()
0 commit comments