"""Tests for the Sonos config flow.""" import asyncio from collections.abc import Callable, Coroutine from http import HTTPStatus from itertools import chain, repeat import logging from typing import Any from unittest.mock import MagicMock, Mock, PropertyMock, patch from freezegun.api import FrozenDateTimeFactory import pytest from requests import Response from requests.exceptions import HTTPError from homeassistant import config_entries from homeassistant.components import sonos from homeassistant.components.sonos.const import ( DISCOVERY_INTERVAL, SONOS_SPEAKER_ACTIVITY, UPNP_ISSUE_ID, ) from homeassistant.components.sonos.exception import SonosUpdateError from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import ( device_registry as dr, entity_registry as er, issue_registry as ir, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from .conftest import MockSoCo, SoCoMockFactory, SonosMockEvent from tests.common import MockConfigEntry, async_fire_time_changed async def test_creating_entry_sets_up_media_player( hass: HomeAssistant, zeroconf_payload: ZeroconfServiceInfo ) -> None: """Test setting up Sonos loads the media player.""" # Initiate a discovery to allow a user config flow await hass.config_entries.flow.async_init( sonos.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf_payload, ) with patch( "homeassistant.components.sonos.media_player.async_setup_entry", ) as mock_setup: result = await hass.config_entries.flow.async_init( sonos.DOMAIN, context={"source": config_entries.SOURCE_USER} ) # Confirmation form assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done(wait_background_tasks=True) assert len(mock_setup.mock_calls) == 1 async def test_configuring_sonos_creates_entry(hass: HomeAssistant) -> None: """Test that specifying config will create an entry.""" with patch( "homeassistant.components.sonos.async_setup_entry", return_value=True, ) as mock_setup: await async_setup_component( hass, sonos.DOMAIN, {"sonos": {"media_player": {"interface_addr": "127.0.0.1"}}}, ) await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 async def test_not_configuring_sonos_not_creates_entry(hass: HomeAssistant) -> None: """Test that no config will not create an entry.""" with patch( "homeassistant.components.sonos.async_setup_entry", return_value=True, ) as mock_setup: await async_setup_component(hass, sonos.DOMAIN, {}) await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 0 async def test_upnp_disabled_discovery( hass: HomeAssistant, config_entry: MockConfigEntry, soco: MockSoCo ) -> None: """Test issue creation when discovery processing fails with 403.""" resp = Response() resp.status_code = HTTPStatus.FORBIDDEN http_error = HTTPError(response=resp) with patch( "tests.components.sonos.conftest.MockSoCo.household_id", new_callable=PropertyMock, create=True, side_effect=http_error, ): config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done(wait_background_tasks=True) issue_registry = ir.async_get(hass) # pylint: disable=home-assistant-tests-registry-fixtures assert ( issue_registry.async_get_issue( sonos.DOMAIN, f"{UPNP_ISSUE_ID}_{soco.ip_address}" ) is not None ) async def test_upnp_disabled_manual_hosts( hass: HomeAssistant, soco_factory: SoCoMockFactory, ) -> None: """Test issue creation when manual host processing fails with 403.""" resp = Response() resp.status_code = HTTPStatus.FORBIDDEN http_error = HTTPError(response=resp) soco = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Bedroom") with patch.object( type(soco), "household_id", new_callable=PropertyMock, create=True, side_effect=http_error, ): await _setup_hass(hass) issue_registry = ir.async_get(hass) # pylint: disable=home-assistant-tests-registry-fixtures issue = issue_registry.async_get_issue( sonos.DOMAIN, f"{UPNP_ISSUE_ID}_{soco.ip_address}" ) assert issue is not None assert issue.translation_placeholders.get("device_ip") == "10.10.10.1" async def test_discovery_exception( hass: HomeAssistant, config_entry: MockConfigEntry, caplog: pytest.LogCaptureFixture, ) -> None: """Test exception handling during discovery processing.""" with patch( "tests.components.sonos.conftest.MockSoCo.household_id", new_callable=PropertyMock, create=True, side_effect=OSError("This is a test"), ): caplog.set_level(logging.ERROR) caplog.clear() config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done(wait_background_tasks=True) assert "This is a test" in caplog.text async def test_discovery_skips_disabled_device( hass: HomeAssistant, config_entry: MockConfigEntry, soco: MockSoCo, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, ) -> None: """Test discovery message handling skips disabled Sonos devices.""" config_entry.add_to_hass(hass) device = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={(sonos.DOMAIN, soco.uid)}, disabled_by=dr.DeviceEntryDisabler.USER, ) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done(wait_background_tasks=True) soco.zoneGroupTopology.subscribe.assert_not_awaited() assert not er.async_entries_for_device(entity_registry, device.id) async def test_discovery_reenable_device_on_new_discovery( hass: HomeAssistant, config_entry: MockConfigEntry, soco: MockSoCo, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, discover: MagicMock, fire_zgs_event: Callable[[], Coroutine[Any, Any, None]], ) -> None: """Test re-enabling a disabled device allows subscriptions and entities.""" config_entry.add_to_hass(hass) device = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={(sonos.DOMAIN, soco.uid)}, disabled_by=dr.DeviceEntryDisabler.USER, ) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done(wait_background_tasks=True) soco.zoneGroupTopology.subscribe.assert_not_awaited() assert not er.async_entries_for_device(entity_registry, device.id) device_registry.async_update_device(device.id, disabled_by=None) # Re-run discovery using the fixture's own mocked callback path. discover.side_effect(*discover.call_args.args, **discover.call_args.kwargs) await hass.async_block_till_done(wait_background_tasks=True) await fire_zgs_event() await hass.async_block_till_done(wait_background_tasks=True) assert soco.zoneGroupTopology.subscribe.await_count > 0 assert er.async_entries_for_device(entity_registry, device.id) async def test_async_poll_manual_hosts_warnings( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, soco_factory: SoCoMockFactory, freezer: FrozenDateTimeFactory, ) -> None: """Test that host warnings are not logged repeatedly.""" soco = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Bedroom") with ( caplog.at_level(logging.DEBUG), patch.object( type(soco), "visible_zones", new_callable=PropertyMock ) as mock_visible_zones, ): # First call fails, it should be logged as a WARNING message mock_visible_zones.side_effect = OSError() caplog.clear() await _setup_hass(hass) assert [ rec.levelname for rec in caplog.records if "Could not get visible Sonos devices from" in rec.message ] == ["WARNING"] # Second call fails again, it should be logged as a DEBUG message mock_visible_zones.side_effect = OSError() caplog.clear() freezer.tick(DISCOVERY_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) assert [ rec.levelname for rec in caplog.records if "Could not get visible Sonos devices from" in rec.message ] == ["DEBUG"] # Third call succeeds, logs message indicating reconnect mock_visible_zones.return_value = {soco} mock_visible_zones.side_effect = None caplog.clear() freezer.tick(DISCOVERY_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) assert [ rec.levelname for rec in caplog.records if "Connection reestablished to Sonos device" in rec.message ] == ["WARNING"] # Fourth call succeeds, it should log nothing caplog.clear() freezer.tick(DISCOVERY_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) assert "Connection reestablished to Sonos device" not in caplog.text # Fifth call fails again again, should be logged as a WARNING message mock_visible_zones.side_effect = OSError() caplog.clear() freezer.tick(DISCOVERY_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() assert [ rec.levelname for rec in caplog.records if "Could not get visible Sonos devices from" in rec.message ] == ["WARNING"] class _MockSoCoOsError(MockSoCo): @property def visible_zones(self): raise OSError class _MockSoCoVisibleZones(MockSoCo): def set_visible_zones(self, visible_zones) -> None: """Set visible zones.""" self.vz_return = visible_zones # pylint: disable=attribute-defined-outside-init @property def visible_zones(self): return self.vz_return class _MockSoCoUidError(MockSoCo): """Mock SoCo used for uid property error tests.""" @property def visible_zones(self): """Return no additional zones without touching uid lookup.""" return set() async def _setup_hass(hass: HomeAssistant): await async_setup_component( hass, sonos.DOMAIN, { "sonos": { "media_player": { "interface_addr": "127.0.0.1", "hosts": ["10.10.10.1", "10.10.10.2"], } } }, ) await hass.async_block_till_done() async def test_async_poll_manual_hosts_1( hass: HomeAssistant, soco_factory: SoCoMockFactory, entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture, ) -> None: """Tests first device fails, second device successful, speakers do not exist.""" soco_1 = soco_factory.cache_mock(_MockSoCoOsError(), "10.10.10.1", "Living Room") soco_2 = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom") with caplog.at_level(logging.WARNING): await _setup_hass(hass) assert "media_player.bedroom" in entity_registry.entities assert "media_player.living_room" not in entity_registry.entities assert ( f"Could not get visible Sonos devices from {soco_1.ip_address}" in caplog.text ) assert ( f"Could not get visible Sonos devices from {soco_2.ip_address}" not in caplog.text ) await hass.async_block_till_done(wait_background_tasks=True) async def test_async_poll_manual_hosts_uid_oserror( hass: HomeAssistant, soco_factory: SoCoMockFactory, entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture, ) -> None: """Test uid lookup OSError skips host and logs warning.""" soco_1 = soco_factory.cache_mock(_MockSoCoUidError(), "10.10.10.1", "Living Room") soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom") uid = soco_1.uid with ( caplog.at_level(logging.WARNING), patch.object( type(soco_1), "uid", new_callable=PropertyMock, create=True, side_effect=chain([uid], repeat(OSError("uid unavailable"))), ), ): await _setup_hass(hass) assert "media_player.bedroom" in entity_registry.entities assert "media_player.living_room" not in entity_registry.entities assert f"Could not get Sonos uid from {soco_1.ip_address}" in caplog.text await hass.async_block_till_done(wait_background_tasks=True) async def test_async_poll_manual_hosts_uid_http_error( hass: HomeAssistant, soco_factory: SoCoMockFactory, entity_registry: er.EntityRegistry, ) -> None: """Test uid lookup HTTPError skips host.""" resp = Response() resp.status_code = HTTPStatus.FORBIDDEN http_error = HTTPError(response=resp) soco_1 = soco_factory.cache_mock(_MockSoCoUidError(), "10.10.10.1", "Living Room") soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom") uid = soco_1.uid with patch.object( type(soco_1), "uid", new_callable=PropertyMock, create=True, side_effect=chain([uid], repeat(http_error)), ): await _setup_hass(hass) assert "media_player.bedroom" in entity_registry.entities assert "media_player.living_room" not in entity_registry.entities await hass.async_block_till_done(wait_background_tasks=True) async def test_async_poll_manual_hosts_2( hass: HomeAssistant, soco_factory: SoCoMockFactory, entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture, ) -> None: """Test first device success, second device fails, speakers do not exist.""" soco_1 = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room") soco_2 = soco_factory.cache_mock(_MockSoCoOsError(), "10.10.10.2", "Bedroom") with caplog.at_level(logging.WARNING): await _setup_hass(hass) assert "media_player.bedroom" not in entity_registry.entities assert "media_player.living_room" in entity_registry.entities assert ( f"Could not get visible Sonos devices from {soco_1.ip_address}" not in caplog.text ) assert ( f"Could not get visible Sonos devices from {soco_2.ip_address}" in caplog.text ) await hass.async_block_till_done(wait_background_tasks=True) async def test_async_poll_manual_hosts_3( hass: HomeAssistant, soco_factory: SoCoMockFactory, entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture, ) -> None: """Test both devices fail, speakers do not exist.""" soco_1 = soco_factory.cache_mock(_MockSoCoOsError(), "10.10.10.1", "Living Room") soco_2 = soco_factory.cache_mock(_MockSoCoOsError(), "10.10.10.2", "Bedroom") with caplog.at_level(logging.WARNING): await _setup_hass(hass) assert "media_player.bedroom" not in entity_registry.entities assert "media_player.living_room" not in entity_registry.entities assert ( f"Could not get visible Sonos devices from {soco_1.ip_address}" in caplog.text ) assert ( f"Could not get visible Sonos devices from {soco_2.ip_address}" in caplog.text ) await hass.async_block_till_done(wait_background_tasks=True) async def test_async_poll_manual_hosts_4( hass: HomeAssistant, soco_factory: SoCoMockFactory, entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture, ) -> None: """Test both devices are successful, speakers do not exist.""" soco_1 = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room") soco_2 = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom") with caplog.at_level(logging.WARNING): await _setup_hass(hass) assert "media_player.bedroom" in entity_registry.entities assert "media_player.living_room" in entity_registry.entities assert ( f"Could not get visible Sonos devices from {soco_1.ip_address}" not in caplog.text ) assert ( f"Could not get visible Sonos devices from {soco_2.ip_address}" not in caplog.text ) await hass.async_block_till_done(wait_background_tasks=True) class SpeakerActivity: """Unit test class to track speaker activity messages.""" def __init__(self, hass: HomeAssistant, soco: MockSoCo) -> None: """Create the object from soco.""" self.soco = soco self.hass = hass self.call_count: int = 0 self.event = asyncio.Event() async_dispatcher_connect( self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}", self.speaker_activity, ) @callback def speaker_activity(self, source: str) -> None: """Track the last activity on this speaker, set availability and resubscribe.""" if source == "manual zone scan": self.event.set() self.call_count += 1 async def test_async_poll_manual_hosts_5( hass: HomeAssistant, soco_factory: SoCoMockFactory, entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture, ) -> None: """Test both succeed, speakers exist and unavailable, ping succeeds.""" soco_1 = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room") soco_1.renderingControl = Mock() soco_1.renderingControl.GetVolume = Mock() 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() speaker_2_activity = SpeakerActivity(hass, soco_2) with caplog.at_level(logging.DEBUG): caplog.clear() await _setup_hass(hass) assert "media_player.bedroom" in entity_registry.entities assert "media_player.living_room" in entity_registry.entities async_fire_time_changed(hass, dt_util.utcnow() + DISCOVERY_INTERVAL) await hass.async_block_till_done() await asyncio.gather( *[speaker_1_activity.event.wait(), speaker_2_activity.event.wait()] ) assert speaker_1_activity.call_count == 1 assert speaker_2_activity.call_count == 1 assert "Activity on Living Room" in caplog.text assert "Activity on Bedroom" in caplog.text await hass.async_block_till_done(wait_background_tasks=True) async def test_async_poll_manual_hosts_6( hass: HomeAssistant, soco_factory: SoCoMockFactory, entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture, ) -> None: """Test both succeed, speakers exist and unavailable, pings fail.""" soco_1 = soco_factory.cache_mock(MockSoCo(), "10.10.10.1", "Living Room") # Rendering Control Get Volume is what speaker ping calls. soco_1.renderingControl = Mock() soco_1.renderingControl.GetVolume = Mock() soco_1.renderingControl.GetVolume.side_effect = SonosUpdateError() 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.renderingControl.GetVolume.side_effect = SonosUpdateError() speaker_2_activity = SpeakerActivity(hass, soco_2) with patch( "homeassistant.components.sonos.DISCOVERY_INTERVAL" ) as mock_discovery_interval: # Speed up manual discovery interval so second iteration runs sooner mock_discovery_interval.total_seconds = Mock(side_effect=[0.0, 60]) await _setup_hass(hass) assert "media_player.bedroom" in entity_registry.entities assert "media_player.living_room" in entity_registry.entities with caplog.at_level(logging.DEBUG): caplog.clear() await hass.async_block_till_done() assert "Activity on Living Room" not in caplog.text assert "Activity on Bedroom" not in caplog.text assert speaker_1_activity.call_count == 0 assert speaker_2_activity.call_count == 0 await hass.async_block_till_done(wait_background_tasks=True) async def test_async_poll_manual_hosts_7( hass: HomeAssistant, soco_factory: SoCoMockFactory, entity_registry: er.EntityRegistry, ) -> None: """Test both succeed, speaker do not exist, new hosts found in visible zones.""" soco_1 = soco_factory.cache_mock( _MockSoCoVisibleZones(), "10.10.10.1", "Living Room" ) soco_2 = soco_factory.cache_mock(_MockSoCoVisibleZones(), "10.10.10.2", "Bedroom") soco_3 = soco_factory.cache_mock(MockSoCo(), "10.10.10.3", "Basement") soco_4 = soco_factory.cache_mock(MockSoCo(), "10.10.10.4", "Garage") soco_5 = soco_factory.cache_mock(MockSoCo(), "10.10.10.5", "Studio") soco_1.set_visible_zones({soco_1, soco_2, soco_3, soco_4, soco_5}) soco_2.set_visible_zones({soco_1, soco_2, soco_3, soco_4, soco_5}) await _setup_hass(hass) await hass.async_block_till_done() assert "media_player.bedroom" in entity_registry.entities assert "media_player.living_room" in entity_registry.entities assert "media_player.basement" in entity_registry.entities assert "media_player.garage" in entity_registry.entities assert "media_player.studio" in entity_registry.entities await hass.async_block_till_done(wait_background_tasks=True) async def test_async_poll_manual_hosts_8( hass: HomeAssistant, soco_factory: SoCoMockFactory, entity_registry: er.EntityRegistry, ) -> None: """Test both succeed, speaker do not exist, invisible zone.""" soco_1 = soco_factory.cache_mock( _MockSoCoVisibleZones(), "10.10.10.1", "Living Room" ) soco_2 = soco_factory.cache_mock(_MockSoCoVisibleZones(), "10.10.10.2", "Bedroom") soco_3 = soco_factory.cache_mock(MockSoCo(), "10.10.10.3", "Basement") soco_4 = soco_factory.cache_mock(MockSoCo(), "10.10.10.4", "Garage") soco_5 = soco_factory.cache_mock(MockSoCo(), "10.10.10.5", "Studio") soco_1.set_visible_zones({soco_2, soco_3, soco_4, soco_5}) soco_2.set_visible_zones({soco_2, soco_3, soco_4, soco_5}) await _setup_hass(hass) await hass.async_block_till_done() assert "media_player.bedroom" in entity_registry.entities assert "media_player.living_room" not in entity_registry.entities assert "media_player.basement" in entity_registry.entities assert "media_player.garage" in entity_registry.entities assert "media_player.studio" in entity_registry.entities await hass.async_block_till_done(wait_background_tasks=True) async def test_async_poll_manual_hosts_skips_disabled_visible_zone( hass: HomeAssistant, soco_factory: SoCoMockFactory, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, ) -> None: """Test disabled visible zone is skipped in visible-zones expansion.""" soco_1 = soco_factory.cache_mock( _MockSoCoVisibleZones(), "10.10.10.1", "Living Room" ) # Host 2 is marked disabled in the device registry. # Host 1's visible-zones expansion encounters host 2 and exercises the # _async_add_visible_zones disabled filter branch. soco_2 = soco_factory.cache_mock(MockSoCo(), "10.10.10.2", "Bedroom") soco_1.set_visible_zones({soco_1, soco_2}) config_entry = MockConfigEntry(domain=sonos.DOMAIN) config_entry.add_to_hass(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={(sonos.DOMAIN, soco_2.uid)}, disabled_by=dr.DeviceEntryDisabler.USER, ) await _setup_hass(hass) await hass.async_block_till_done(wait_background_tasks=True) assert "media_player.living_room" in entity_registry.entities assert "media_player.bedroom" not in entity_registry.entities await hass.async_block_till_done(wait_background_tasks=True) async def _setup_hass_ipv6_address_not_supported(hass: HomeAssistant): await async_setup_component( hass, sonos.DOMAIN, { "sonos": { "media_player": { "interface_addr": "127.0.0.1", "hosts": ["2001:db8:3333:4444:5555:6666:7777:8888"], } } }, ) await hass.async_block_till_done() async def test_ipv6_not_supported( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, ) -> None: """Tests that invalid ipv4 addresses do not generate stack dump.""" with caplog.at_level(logging.DEBUG): caplog.clear() await _setup_hass_ipv6_address_not_supported(hass) await hass.async_block_till_done() assert "invalid ip_address received" in caplog.text assert "2001:db8:3333:4444:5555:6666:7777:8888" in caplog.text async def test_no_exception_when_entry_unloaded_during_speaker_discovery( hass: HomeAssistant, config_entry: MockConfigEntry, soco: MockSoCo, soco_factory: SoCoMockFactory, async_autosetup_sonos, caplog: pytest.LogCaptureFixture, ) -> None: """Test no exception when a discovery event arrives after entry unload. Exercises the scenario where async_add_speakers is called after the entry has been unloaded. The _stop_event guard in _add_speaker must detect that the entry is inactive and skip speaker creation without raising an exception. """ new_soco = soco_factory.cache_mock(MockSoCo(), "10.10.10.99", "New Room") assert await hass.config_entries.async_unload(config_entry.entry_id) # Fire a ZGS discovery event for the new speaker after the entry is # unloaded. This simulates a late-arriving network event. subscription = soco.zoneGroupTopology.subscribe.return_value sub_callback = await subscription.wait_for_callback_to_be_set() with ( patch.object( MockSoCo, "visible_zones", new_callable=PropertyMock, return_value={soco, new_soco}, ), caplog.at_level(logging.DEBUG), ): caplog.clear() sub_callback(SonosMockEvent(soco, soco.zoneGroupTopology, {})) await hass.async_block_till_done(wait_background_tasks=True) assert any( "Config entry unloaded while adding speaker" in message for message in caplog.messages )