diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index d3de2a889d5..a2a4e53cae4 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -214,12 +214,25 @@ class MockSoCo(MagicMock): surround_level = 3 music_surround_level = 4 soundbar_audio_input_format = "Dolby 5.1" + factory: SoCoMockFactory | None = None @property def visible_zones(self): """Return visible zones and allow property to be overridden by device classes.""" return {self} + @property + def all_zones(self) -> set[MockSoCo]: + """Return a set of all mock zones, or just self if no factory or zones.""" + if self.factory is not None: + if zones := self.factory.mock_all_zones: + return zones + return {self} + + def set_factory(self, factory: SoCoMockFactory) -> None: + """Set the factory for this mock.""" + self.factory = factory + class SoCoMockFactory: """Factory for creating SoCo Mocks.""" @@ -244,11 +257,19 @@ class SoCoMockFactory: self.sonos_playlists = sonos_playlists self.sonos_queue = sonos_queue + @property + def mock_all_zones(self) -> set[MockSoCo]: + """Return a set of all mock zones.""" + return { + mock for mock in self.mock_list.values() if mock.mock_include_in_all_zones + } + def cache_mock( self, mock_soco: MockSoCo, ip_address: str, name: str = "Zone A" ) -> MockSoCo: """Put a user created mock into the cache.""" mock_soco.mock_add_spec(SoCo) + mock_soco.set_factory(self) mock_soco.ip_address = ip_address if ip_address != "192.168.42.2": mock_soco.uid += f"_{ip_address}" @@ -260,6 +281,11 @@ class SoCoMockFactory: my_speaker_info = self.speaker_info.copy() my_speaker_info["zone_name"] = name my_speaker_info["uid"] = mock_soco.uid + # Generate a different MAC for the non-default speakers. + # otherwise new devices will not be created. + if ip_address != "192.168.42.2": + last_octet = ip_address.split(".")[-1] + my_speaker_info["mac_address"] = f"00-00-00-00-00-{last_octet.zfill(2)}" mock_soco.get_speaker_info = Mock(return_value=my_speaker_info) mock_soco.add_to_queue = Mock(return_value=10) mock_soco.add_uri_to_queue = Mock(return_value=10) @@ -278,7 +304,7 @@ class SoCoMockFactory: mock_soco.alarmClock = self.alarm_clock mock_soco.get_battery_info.return_value = self.battery_info - mock_soco.all_zones = {mock_soco} + mock_soco.mock_include_in_all_zones = True mock_soco.group.coordinator = mock_soco mock_soco.household_id = "test_household_id" self.mock_list[ip_address] = mock_soco diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index c1b98b2ec60..901ae359917 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -324,10 +324,15 @@ async def test_async_poll_manual_hosts_5( soco_1 = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room") soco_1.renderingControl = Mock() soco_1.renderingControl.GetVolume = Mock() + # Unavailable speakers should not be included in all zones + soco_1.mock_include_in_all_zones = False + speaker_1_activity = SpeakerActivity(hass, soco_1) soco_2 = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom") soco_2.renderingControl = Mock() soco_2.renderingControl.GetVolume = Mock() + soco_2.mock_include_in_all_zones = False + speaker_2_activity = SpeakerActivity(hass, soco_2) with caplog.at_level(logging.DEBUG): diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index 04457ee95c7..56dd96b0caf 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -26,10 +26,10 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import dt as dt_util -from .conftest import MockSoCo, SonosMockEvent +from .conftest import MockSoCo, SonosMockEvent, SonosMockService from tests.common import async_fire_time_changed @@ -211,3 +211,53 @@ async def test_alarm_create_delete( assert "switch.sonos_alarm_14" in entity_registry.entities assert "switch.sonos_alarm_15" not in entity_registry.entities + + +async def test_alarm_change_device( + hass: HomeAssistant, + async_setup_sonos, + soco: MockSoCo, + alarm_clock: SonosMockService, + alarm_clock_extended: SonosMockService, + alarm_event: SonosMockEvent, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + sonos_setup_two_speakers: list[MockSoCo], +) -> None: + """Test Sonos Alarm being moved to a different speaker. + + This test simulates a scenario where an alarm is created on one speaker + and then moved to another speaker. It checks that the entity is correctly + created on the new speaker and removed from the old one. + """ + entity_id = "switch.sonos_alarm_14" + soco_lr = sonos_setup_two_speakers[0] + + await async_setup_sonos() + + # Initially, the alarm is created on the soco mock + assert entity_id in entity_registry.entities + entity = entity_registry.async_get(entity_id) + device = device_registry.async_get(entity.device_id) + assert device.name == soco.get_speaker_info()["zone_name"] + + # Simulate the alarm being moved to the soco_lr speaker + alarm_update = copy(alarm_clock_extended.ListAlarms.return_value) + alarm_update["CurrentAlarmList"] = alarm_update["CurrentAlarmList"].replace( + "RINCON_test", f"{soco_lr.uid}" + ) + alarm_clock.ListAlarms.return_value = alarm_update + + # Update the alarm_list_version so it gets processed. + alarm_event.variables["alarm_list_version"] = f"{soco_lr.uid}:1000" + alarm_update["CurrentAlarmListVersion"] = alarm_event.increment_variable( + "alarm_list_version" + ) + + alarm_clock.subscribe.return_value.callback(event=alarm_event) + await hass.async_block_till_done(wait_background_tasks=True) + + assert entity_id in entity_registry.entities + alarm_14 = entity_registry.async_get(entity_id) + device = device_registry.async_get(alarm_14.device_id) + assert device.name == soco_lr.get_speaker_info()["zone_name"]