From a91568f5af311ac58f0d89b1edd74174eba02581 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Apr 2021 13:46:46 -1000 Subject: [PATCH 1/9] Cancel config entry and platform retry at the stop event The retries would block shutdown. --- homeassistant/bootstrap.py | 20 +++++++++++++++-- homeassistant/config_entries.py | 25 +++++++++++++++++---- homeassistant/helpers/entity_platform.py | 28 +++++++++++++++++++++--- 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index fc12ec065a9bd8..a924cba901c821 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -17,9 +17,18 @@ from homeassistant import config as conf_util, config_entries, core, loader from homeassistant.components import http -from homeassistant.const import REQUIRED_NEXT_PYTHON_DATE, REQUIRED_NEXT_PYTHON_VER +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, + REQUIRED_NEXT_PYTHON_DATE, + REQUIRED_NEXT_PYTHON_VER, +) from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import area_registry, device_registry, entity_registry +from homeassistant.helpers import ( + area_registry, + device_registry, + entity_platform, + entity_registry, +) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType from homeassistant.setup import ( @@ -440,6 +449,13 @@ async def _async_set_up_integrations( hass.data[DATA_SETUP_STARTED] = {} setup_time = hass.data[DATA_SETUP_TIME] = {} + async def _async_shutdown_entity_platforms(event: core.Event) -> None: + await entity_platform.async_shutdown(hass) + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_shutdown_entity_platforms + ) + watch_task = asyncio.create_task(_async_watch_pending_setups(hass)) domains_to_setup = _get_domains(hass, config) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index d689d4548a9d6e..34afc77e52809b 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -12,7 +12,7 @@ import attr from homeassistant import data_entry_flow, loader -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback from homeassistant.exceptions import ( ConfigEntryAuthFailed, @@ -331,6 +331,17 @@ async def setup_again(*_: Any) -> None: else: self.state = ENTRY_STATE_SETUP_ERROR + async def async_shutdown(self) -> None: + """Call when Home Assistant is stopping.""" + self.async_cancel_retry_setup() + + @callback + def async_cancel_retry_setup(self) -> None: + """Cancel retry setup.""" + if self._async_cancel_retry_setup is not None: + self._async_cancel_retry_setup() + self._async_cancel_retry_setup = None + async def async_unload( self, hass: HomeAssistant, *, integration: loader.Integration | None = None ) -> bool: @@ -360,9 +371,7 @@ async def async_unload( return False if self.state != ENTRY_STATE_LOADED: - if self._async_cancel_retry_setup is not None: - self._async_cancel_retry_setup() - self._async_cancel_retry_setup = None + self.async_cancel_retry_setup() self.state = ENTRY_STATE_NOT_LOADED return True @@ -778,6 +787,12 @@ async def async_remove(self, entry_id: str) -> dict[str, Any]: return {"require_restart": not unload_success} + async def _async_shutdown(self, event: Event) -> None: + """Call when Home Assistant is stopping.""" + await asyncio.gather( + *[entry.async_shutdown() for entry in self._entries.values()] + ) + async def async_initialize(self) -> None: """Initialize config entry config.""" # Migrating for config entries stored before 0.73 @@ -787,6 +802,8 @@ async def async_initialize(self) -> None: old_conf_migrate_func=_old_conf_migrator, ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown) + if config is None: self._entries = {} return diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 25996c81d9d32f..019aaccd4821cc 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -4,6 +4,7 @@ import asyncio from contextvars import ContextVar from datetime import datetime, timedelta +import itertools import logging from logging import Logger from types import ModuleType @@ -57,6 +58,18 @@ _LOGGER = logging.getLogger(__name__) +async def async_shutdown(hass: HomeAssistant) -> None: + """Call when Home Assistant is stopping.""" + if DATA_ENTITY_PLATFORM not in hass.data: + return + await asyncio.gather( + *[ + platform.async_shutdown() + for platform in itertools.chain(*hass.data[DATA_ENTITY_PLATFORM].values()) + ] + ) + + class EntityPlatform: """Manage the entities for a single platform.""" @@ -174,6 +187,17 @@ def async_create_setup_task() -> Coroutine: await self._async_setup_platform(async_create_setup_task) + async def async_shutdown(self) -> None: + """Call when Home Assistant is stopping.""" + self.async_cancel_retry_setup() + + @callback + def async_cancel_retry_setup(self) -> None: + """Cancel retry setup.""" + if self._async_cancel_retry_setup is not None: + self._async_cancel_retry_setup() + self._async_cancel_retry_setup = None + async def async_setup_entry(self, config_entry: config_entries.ConfigEntry) -> bool: """Set up the platform from a config entry.""" # Store it so that we can save config entry ID in entity registry @@ -549,9 +573,7 @@ async def async_reset(self) -> None: This method must be run in the event loop. """ - if self._async_cancel_retry_setup is not None: - self._async_cancel_retry_setup() - self._async_cancel_retry_setup = None + self.async_cancel_retry_setup() if not self.entities: return From 1be73df8d1576bb8fca74b101976209fe7b767ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Apr 2021 14:17:44 -1000 Subject: [PATCH 2/9] .. and stop polling --- homeassistant/helpers/entity_platform.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 019aaccd4821cc..8ef03702314f2b 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -190,6 +190,7 @@ def async_create_setup_task() -> Coroutine: async def async_shutdown(self) -> None: """Call when Home Assistant is stopping.""" self.async_cancel_retry_setup() + self.async_unsub_polling() @callback def async_cancel_retry_setup(self) -> None: @@ -582,10 +583,15 @@ async def async_reset(self) -> None: await asyncio.gather(*tasks) + self.async_unsub_polling() + self._setup_complete = False + + @callback + def async_unsub_polling(self) -> None: + """Stop polling.""" if self._async_unsub_polling is not None: self._async_unsub_polling() self._async_unsub_polling = None - self._setup_complete = False async def async_destroy(self) -> None: """Destroy an entity platform. From 6cd17ef5cfc2d4c63a85fea326838168b5b9d976 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Apr 2021 21:54:59 -1000 Subject: [PATCH 3/9] coverage --- tests/test_bootstrap.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 24646386278974..388c81f837bc6a 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -8,7 +8,9 @@ from homeassistant import bootstrap, core, runner import homeassistant.config as config_util +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_platform import homeassistant.util.dt as dt_util from tests.common import ( @@ -610,3 +612,26 @@ async def test_setup_safe_mode_if_no_frontend( assert hass.config.skip_pip assert hass.config.internal_url == "http://192.168.1.100:8123" assert hass.config.external_url == "https://abcdef.ui.nabu.casa" + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_stop_shuts_down_platforms(hass): + """Test entity_platform.async_shutdown is called on the stop event.""" + + mock_integration( + hass, + MockModule( + domain="normal_integration", + partial_manifest={"after_dependencies": ["an_after_dep"]}, + ), + ) + + await bootstrap._async_set_up_integrations(hass, {"normal_integration": {}}) + + assert "normal_integration" in hass.config.components + + with patch.object(entity_platform, "async_shutdown") as mock_async_shutdown: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert mock_async_shutdown.called From 7d89af91ff64d93a8ad5145e922dc34a32ff3651 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Apr 2021 22:00:15 -1000 Subject: [PATCH 4/9] coverage --- tests/test_config_entries.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index dbfe48129c1aa5..9b3f0c00ccbaea 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -7,7 +7,7 @@ import pytest from homeassistant import config_entries, data_entry_flow, loader -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState, callback from homeassistant.exceptions import ( ConfigEntryAuthFailed, @@ -2667,3 +2667,15 @@ async def _async_update_data(): assert entry.state == config_entries.ENTRY_STATE_LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 + + +async def test_initialize_and_shutdown(hass): + """Test we call the shutdown function at stop.""" + manager = config_entries.ConfigEntries(hass, {}) + + with patch.object(manager, "_async_shutdown") as mock_async_shutdown: + await manager.async_initialize() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert mock_async_shutdown.called From d9059baa82dfa8ee53fc4ce3753431636bdaaf9c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Apr 2021 22:06:31 -1000 Subject: [PATCH 5/9] coverage --- tests/test_config_entries.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 9b3f0c00ccbaea..20ab5e67fef6c7 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1405,7 +1405,7 @@ async def test_reload_entry_entity_registry_works(hass): assert len(mock_unload_entry.mock_calls) == 1 -async def test_unqiue_id_persisted(hass, manager): +async def test_unique_id_persisted(hass, manager): """Test that a unique ID is stored in the config entry.""" mock_setup_entry = AsyncMock(return_value=True) @@ -2679,3 +2679,28 @@ async def test_initialize_and_shutdown(hass): await hass.async_block_till_done() assert mock_async_shutdown.called + + +async def test_setup_retrying_during_shutdown(hass): + """Test if we shutdown an entry that is in retry mode.""" + entry = MockConfigEntry(domain="test") + + mock_setup_entry = AsyncMock(side_effect=ConfigEntryNotReady) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + with patch("homeassistant.helpers.event.async_call_later") as mock_call: + await entry.async_setup(hass) + + assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert len(mock_call.return_value.mock_calls) == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert len(mock_call.return_value.mock_calls) == 0 + + async_fire_time_changed(hass, dt.utcnow() + timedelta(hours=4)) + await hass.async_block_till_done() + + assert len(mock_call.return_value.mock_calls) == 0 From 2586d3c5348172261520b80f95a639f599947a2d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Apr 2021 22:18:23 -1000 Subject: [PATCH 6/9] coverage --- tests/helpers/test_entity_platform.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index e842d5aa1ae6b7..d24084ff517144 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -685,6 +685,29 @@ async def test_reset_cancels_retry_setup_when_not_started(hass): assert ent_platform._async_cancel_retry_setup is None +async def test_stop_shutdown_cancels_retry_setup_and_interval_listener(hass): + """Test that shutdown will cancel scheduled a setup retry and interval listener.""" + async_setup_entry = Mock(side_effect=PlatformNotReady) + platform = MockPlatform(async_setup_entry=async_setup_entry) + config_entry = MockConfigEntry() + ent_platform = MockEntityPlatform( + hass, platform_name=config_entry.domain, platform=platform + ) + + with patch.object(entity_platform, "async_call_later") as mock_call_later: + assert not await ent_platform.async_setup_entry(config_entry) + + assert len(mock_call_later.mock_calls) == 1 + assert len(mock_call_later.return_value.mock_calls) == 0 + assert ent_platform._async_cancel_retry_setup is not None + + await ent_platform.async_shutdown() + + assert len(mock_call_later.return_value.mock_calls) == 1 + assert ent_platform._async_unsub_polling is None + assert ent_platform._async_cancel_retry_setup is None + + async def test_not_fails_with_adding_empty_entities_(hass): """Test for not fails on empty entities list.""" component = EntityComponent(_LOGGER, DOMAIN, hass) From 9c15bdeb47d8e8c8eae8d13581390832b3f3bce4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Apr 2021 12:11:23 -1000 Subject: [PATCH 7/9] move entity platform shutdown to be per component --- homeassistant/bootstrap.py | 20 ++---------------- homeassistant/helpers/entity_component.py | 16 ++++++++++++++- homeassistant/helpers/entity_platform.py | 13 ------------ tests/test_bootstrap.py | 25 ----------------------- 4 files changed, 17 insertions(+), 57 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index a924cba901c821..fc12ec065a9bd8 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -17,18 +17,9 @@ from homeassistant import config as conf_util, config_entries, core, loader from homeassistant.components import http -from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, - REQUIRED_NEXT_PYTHON_DATE, - REQUIRED_NEXT_PYTHON_VER, -) +from homeassistant.const import REQUIRED_NEXT_PYTHON_DATE, REQUIRED_NEXT_PYTHON_VER from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import ( - area_registry, - device_registry, - entity_platform, - entity_registry, -) +from homeassistant.helpers import area_registry, device_registry, entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType from homeassistant.setup import ( @@ -449,13 +440,6 @@ async def _async_set_up_integrations( hass.data[DATA_SETUP_STARTED] = {} setup_time = hass.data[DATA_SETUP_TIME] = {} - async def _async_shutdown_entity_platforms(event: core.Event) -> None: - await entity_platform.async_shutdown(hass) - - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_shutdown_entity_platforms - ) - watch_task = asyncio.create_task(_async_watch_pending_setups(hass)) domains_to_setup = _get_domains(hass, config) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 17131665240466..cdee8dc77d6b6c 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -12,7 +12,11 @@ from homeassistant import config as conf_util from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_SCAN_INTERVAL +from homeassistant.const import ( + CONF_ENTITY_NAMESPACE, + CONF_SCAN_INTERVAL, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( @@ -118,6 +122,8 @@ async def async_setup(self, config: ConfigType) -> None: This method must be run in the event loop. """ + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown) + self.config = config # Look in config for Domain, Domain 2, Domain 3 etc and load them @@ -322,3 +328,11 @@ def _async_init_entity_platform( scan_interval=scan_interval, entity_namespace=entity_namespace, ) + + async def _async_shutdown(self) -> None: + """Call when Home Assistant is stopping.""" + if not self._platforms: + return + await asyncio.gather( + *[platform.async_shutdown() for platform in chain(self._platforms.values())] + ) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 8ef03702314f2b..ef45b8dcd97a50 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -4,7 +4,6 @@ import asyncio from contextvars import ContextVar from datetime import datetime, timedelta -import itertools import logging from logging import Logger from types import ModuleType @@ -58,18 +57,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_shutdown(hass: HomeAssistant) -> None: - """Call when Home Assistant is stopping.""" - if DATA_ENTITY_PLATFORM not in hass.data: - return - await asyncio.gather( - *[ - platform.async_shutdown() - for platform in itertools.chain(*hass.data[DATA_ENTITY_PLATFORM].values()) - ] - ) - - class EntityPlatform: """Manage the entities for a single platform.""" diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 388c81f837bc6a..24646386278974 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -8,9 +8,7 @@ from homeassistant import bootstrap, core, runner import homeassistant.config as config_util -from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_platform import homeassistant.util.dt as dt_util from tests.common import ( @@ -612,26 +610,3 @@ async def test_setup_safe_mode_if_no_frontend( assert hass.config.skip_pip assert hass.config.internal_url == "http://192.168.1.100:8123" assert hass.config.external_url == "https://abcdef.ui.nabu.casa" - - -@pytest.mark.parametrize("load_registries", [False]) -async def test_stop_shuts_down_platforms(hass): - """Test entity_platform.async_shutdown is called on the stop event.""" - - mock_integration( - hass, - MockModule( - domain="normal_integration", - partial_manifest={"after_dependencies": ["an_after_dep"]}, - ), - ) - - await bootstrap._async_set_up_integrations(hass, {"normal_integration": {}}) - - assert "normal_integration" in hass.config.components - - with patch.object(entity_platform, "async_shutdown") as mock_async_shutdown: - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - - assert mock_async_shutdown.called From 77de50a85e3135f2344be077ebfef3fd4a48c033 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Apr 2021 12:35:54 -1000 Subject: [PATCH 8/9] fix --- homeassistant/helpers/entity_component.py | 4 ++-- tests/helpers/test_entity_component.py | 28 ++++++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index cdee8dc77d6b6c..4f2f10bab64f30 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -17,7 +17,7 @@ CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import Event, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( config_per_platform, @@ -329,7 +329,7 @@ def _async_init_entity_platform( entity_namespace=entity_namespace, ) - async def _async_shutdown(self) -> None: + async def _async_shutdown(self, event: Event) -> None: """Call when Home Assistant is stopping.""" if not self._platforms: return diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 8d61ec7d509ff7..1d18111b0d34de 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -8,7 +8,11 @@ import pytest import voluptuous as vol -from homeassistant.const import ENTITY_MATCH_ALL, ENTITY_MATCH_NONE +from homeassistant.const import ( + ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, + EVENT_HOMEASSISTANT_STOP, +) import homeassistant.core as ha from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import discovery @@ -487,3 +491,25 @@ def appender(**kwargs): DOMAIN, "hello", {"area_id": ENTITY_MATCH_NONE, "some": "data"}, blocking=True ) assert len(calls) == 2 + + +async def test_platforms_shutdown_on_stop(hass): + """Test that we shutdown platforms on stop.""" + platform1_setup = Mock(side_effect=[PlatformNotReady, PlatformNotReady, None]) + mock_integration(hass, MockModule("mod1")) + mock_entity_platform(hass, "test_domain.mod1", MockPlatform(platform1_setup)) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + await component.async_setup({DOMAIN: {"platform": "mod1"}}) + await hass.async_block_till_done() + assert len(platform1_setup.mock_calls) == 1 + assert "test_domain.mod1" not in hass.config.components + + with patch.object( + component._platforms[DOMAIN], "async_shutdown" + ) as mock_async_shutdown: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert mock_async_shutdown.called From 1329d3fd308a08f9346a2bd05829ffe829743736 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Apr 2021 15:24:15 -1000 Subject: [PATCH 9/9] Remove unreachable code --- homeassistant/helpers/entity_component.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 4f2f10bab64f30..46279fcb14010e 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -331,8 +331,6 @@ def _async_init_entity_platform( async def _async_shutdown(self, event: Event) -> None: """Call when Home Assistant is stopping.""" - if not self._platforms: - return await asyncio.gather( *[platform.async_shutdown() for platform in chain(self._platforms.values())] )