From 86fde1a644ef5ecde6fc66b90cd0c090feaa847f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 22 Jun 2022 10:56:17 -0500 Subject: [PATCH] Handle failures during initial Sonos subscription (#73456) --- homeassistant/components/sonos/exception.py | 4 +++ homeassistant/components/sonos/speaker.py | 39 +++++++++++++-------- tests/components/sonos/test_speaker.py | 18 ++++++++++ 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/sonos/exception.py b/homeassistant/components/sonos/exception.py index dd2d30796cc..7ff5dacd293 100644 --- a/homeassistant/components/sonos/exception.py +++ b/homeassistant/components/sonos/exception.py @@ -7,6 +7,10 @@ class UnknownMediaType(BrowseError): """Unknown media type.""" +class SonosSubscriptionsFailed(HomeAssistantError): + """Subscription creation failed.""" + + class SonosUpdateError(HomeAssistantError): """Update failed.""" diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 93d0afbcf9c..d37e3bac2a3 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -57,7 +57,7 @@ from .const import ( SONOS_VANISHED, SUBSCRIPTION_TIMEOUT, ) -from .exception import S1BatteryMissing, SonosUpdateError +from .exception import S1BatteryMissing, SonosSubscriptionsFailed, SonosUpdateError from .favorites import SonosFavorites from .helpers import soco_error from .media import SonosMedia @@ -324,12 +324,29 @@ class SonosSpeaker: async with self._subscription_lock: if self._subscriptions: return - await self._async_subscribe() + try: + await self._async_subscribe() + except SonosSubscriptionsFailed: + _LOGGER.warning("Creating subscriptions failed for %s", self.zone_name) + await self._async_offline() async def _async_subscribe(self) -> None: """Create event subscriptions.""" _LOGGER.debug("Creating subscriptions for %s", self.zone_name) + subscriptions = [ + self._subscribe(getattr(self.soco, service), self.async_dispatch_event) + for service in SUBSCRIPTION_SERVICES + ] + results = await asyncio.gather(*subscriptions, return_exceptions=True) + for result in results: + self.log_subscription_result( + result, "Creating subscription", logging.WARNING + ) + + if any(isinstance(result, Exception) for result in results): + raise SonosSubscriptionsFailed + # Create a polling task in case subscriptions fail or callback events do not arrive if not self._poll_timer: self._poll_timer = async_track_time_interval( @@ -342,16 +359,6 @@ class SonosSpeaker: SCAN_INTERVAL, ) - subscriptions = [ - self._subscribe(getattr(self.soco, service), self.async_dispatch_event) - for service in SUBSCRIPTION_SERVICES - ] - results = await asyncio.gather(*subscriptions, return_exceptions=True) - for result in results: - self.log_subscription_result( - result, "Creating subscription", logging.WARNING - ) - async def _subscribe( self, target: SubscriptionBase, sub_callback: Callable ) -> None: @@ -585,6 +592,11 @@ class SonosSpeaker: await self.async_offline() async def async_offline(self) -> None: + """Handle removal of speaker when unavailable.""" + async with self._subscription_lock: + await self._async_offline() + + async def _async_offline(self) -> None: """Handle removal of speaker when unavailable.""" if not self.available: return @@ -602,8 +614,7 @@ class SonosSpeaker: self._poll_timer() self._poll_timer = None - async with self._subscription_lock: - await self.async_unsubscribe() + await self.async_unsubscribe() self.hass.data[DATA_SONOS].discovery_known.discard(self.soco.uid) diff --git a/tests/components/sonos/test_speaker.py b/tests/components/sonos/test_speaker.py index 96b3d222dc6..e47540a6aab 100644 --- a/tests/components/sonos/test_speaker.py +++ b/tests/components/sonos/test_speaker.py @@ -29,3 +29,21 @@ async def test_fallback_to_polling( assert speaker.subscriptions_failed assert "falling back to polling" in caplog.text assert "Activity on Zone A from SonosSpeaker.update_volume" in caplog.text + + +async def test_subscription_creation_fails(hass: HomeAssistant, async_setup_sonos): + """Test that subscription creation failures are handled.""" + with patch( + "homeassistant.components.sonos.speaker.SonosSpeaker._subscribe", + side_effect=ConnectionError("Took too long"), + ): + await async_setup_sonos() + + speaker = list(hass.data[DATA_SONOS].discovered.values())[0] + assert not speaker._subscriptions + + with patch.object(speaker, "_resub_cooldown_expires_at", None): + speaker.speaker_activity("discovery") + await hass.async_block_till_done() + + assert speaker._subscriptions