From b5c679f3d039a72836ebaf9c03868129f45ff5a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Apr 2021 00:31:58 -1000 Subject: [PATCH] Apply ConfigEntryNotReady improvements to PlatformNotReady (#48665) * Apply ConfigEntryNotReady improvements to PlatformNotReady - Limit log spam #47201 - Log exception reason #48449 - Prevent startup blockage #48660 * coverage --- homeassistant/helpers/entity_platform.py | 45 ++++++++++---- tests/helpers/test_entity_platform.py | 75 +++++++++++++++++++++++- 2 files changed, 107 insertions(+), 13 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index b9d603ba5e1..00783b072c9 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -9,9 +9,14 @@ from types import ModuleType from typing import TYPE_CHECKING, Callable, Coroutine, Iterable from homeassistant import config_entries -from homeassistant.const import ATTR_RESTORED, DEVICE_DEFAULT_NAME +from homeassistant.const import ( + ATTR_RESTORED, + DEVICE_DEFAULT_NAME, + EVENT_HOMEASSISTANT_STARTED, +) from homeassistant.core import ( CALLBACK_TYPE, + CoreState, HomeAssistant, ServiceCall, callback, @@ -215,23 +220,41 @@ class EntityPlatform: hass.config.components.add(full_name) self._setup_complete = True return True - except PlatformNotReady: + except PlatformNotReady as ex: tries += 1 wait_time = min(tries, 6) * PLATFORM_NOT_READY_BASE_WAIT_TIME - logger.warning( - "Platform %s not ready yet. Retrying in %d seconds.", - self.platform_name, - wait_time, - ) + message = str(ex) + if not message and ex.__cause__: + message = str(ex.__cause__) + ready_message = f"ready yet: {message}" if message else "ready yet" + if tries == 1: + logger.warning( + "Platform %s not %s; Retrying in background in %d seconds", + self.platform_name, + ready_message, + wait_time, + ) + else: + logger.debug( + "Platform %s not %s; Retrying in %d seconds", + self.platform_name, + ready_message, + wait_time, + ) - async def setup_again(now): # type: ignore[no-untyped-def] + async def setup_again(*_): # type: ignore[no-untyped-def] """Run setup again.""" self._async_cancel_retry_setup = None await self._async_setup_platform(async_create_setup_task, tries) - self._async_cancel_retry_setup = async_call_later( - hass, wait_time, setup_again - ) + if hass.state == CoreState.running: + self._async_cancel_retry_setup = async_call_later( + hass, wait_time, setup_again + ) + else: + self._async_cancel_retry_setup = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, setup_again + ) return False except asyncio.TimeoutError: logger.error( diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 3f26535de18..e842d5aa1ae 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -6,8 +6,8 @@ from unittest.mock import Mock, patch import pytest -from homeassistant.const import PERCENTAGE -from homeassistant.core import callback +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, PERCENTAGE +from homeassistant.core import CoreState, callback from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers import ( device_registry as dr, @@ -592,6 +592,52 @@ async def test_setup_entry_platform_not_ready(hass, caplog): assert len(mock_call_later.mock_calls) == 1 +async def test_setup_entry_platform_not_ready_with_message(hass, caplog): + """Test when an entry is not ready yet that includes a message.""" + async_setup_entry = Mock(side_effect=PlatformNotReady("lp0 on fire")) + 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) + + full_name = f"{ent_platform.domain}.{config_entry.domain}" + assert full_name not in hass.config.components + assert len(async_setup_entry.mock_calls) == 1 + + assert "Platform test not ready yet" in caplog.text + assert "lp0 on fire" in caplog.text + assert len(mock_call_later.mock_calls) == 1 + + +async def test_setup_entry_platform_not_ready_from_exception(hass, caplog): + """Test when an entry is not ready yet that includes the causing exception string.""" + original_exception = HomeAssistantError("The device dropped the connection") + platform_exception = PlatformNotReady() + platform_exception.__cause__ = original_exception + + async_setup_entry = Mock(side_effect=platform_exception) + 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) + + full_name = f"{ent_platform.domain}.{config_entry.domain}" + assert full_name not in hass.config.components + assert len(async_setup_entry.mock_calls) == 1 + + assert "Platform test not ready yet" in caplog.text + assert "The device dropped the connection" in caplog.text + assert len(mock_call_later.mock_calls) == 1 + + async def test_reset_cancels_retry_setup(hass): """Test that resetting a platform will cancel scheduled a setup retry.""" async_setup_entry = Mock(side_effect=PlatformNotReady) @@ -614,6 +660,31 @@ async def test_reset_cancels_retry_setup(hass): assert ent_platform._async_cancel_retry_setup is None +async def test_reset_cancels_retry_setup_when_not_started(hass): + """Test that resetting a platform will cancel scheduled a setup retry when not yet started.""" + hass.state = CoreState.starting + async_setup_entry = Mock(side_effect=PlatformNotReady) + initial_listeners = hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] + + platform = MockPlatform(async_setup_entry=async_setup_entry) + config_entry = MockConfigEntry() + ent_platform = MockEntityPlatform( + hass, platform_name=config_entry.domain, platform=platform + ) + + assert not await ent_platform.async_setup_entry(config_entry) + await hass.async_block_till_done() + assert ( + hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] == initial_listeners + 1 + ) + assert ent_platform._async_cancel_retry_setup is not None + + await ent_platform.async_reset() + await hass.async_block_till_done() + assert hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] == initial_listeners + 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)