From eba7cad33f7de72fe0bb5fe4fdd76897a15534da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Oct 2021 10:41:56 -1000 Subject: [PATCH] Fix yeelight connection when bulb stops responding to SSDP (#57138) --- homeassistant/components/yeelight/__init__.py | 21 +++--- tests/components/yeelight/test_init.py | 67 +++++++++++-------- 2 files changed, 53 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index e7f7b06f58f..a1dce44893b 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -181,6 +181,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DATA_CUSTOM_EFFECTS: conf.get(CONF_CUSTOM_EFFECTS, {}), DATA_CONFIG_ENTRIES: {}, } + # Make sure the scanner is always started in case we are + # going to retry via ConfigEntryNotReady and the bulb has changed + # ip + scanner = YeelightScanner.async_get(hass) + await scanner.async_setup() # Import manually configured devices for host, device_config in config.get(DOMAIN, {}).get(CONF_DEVICES, {}).items(): @@ -281,11 +286,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: device = await _async_get_device(hass, entry.data[CONF_HOST], entry) except BULB_EXCEPTIONS as ex: - # If CONF_ID is not valid we cannot fallback to discovery - # so we must retry by raising ConfigEntryNotReady - if not entry.data.get(CONF_ID): - raise ConfigEntryNotReady from ex - # Otherwise fall through to discovery + # Always retry later since bulbs can stop responding to SSDP + # sometimes even though they are online. If it has changed + # IP we will update it via discovery to the config flow + raise ConfigEntryNotReady from ex else: # Since device is passed this cannot throw an exception anymore await _async_initialize(hass, entry, entry.data[CONF_HOST], device=device) @@ -298,7 +302,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except BULB_EXCEPTIONS: _LOGGER.exception("Failed to connect to bulb at %s", host) - # discovery scanner = YeelightScanner.async_get(hass) await scanner.async_register_callback(entry.data[CONF_ID], _async_from_discovery) return True @@ -501,7 +504,9 @@ class YeelightScanner: _LOGGER.debug("Discovered via SSDP: %s", response) unique_id = response["id"] host = urlparse(response["location"]).hostname - if unique_id not in self._unique_id_capabilities: + current_entry = self._unique_id_capabilities.get(unique_id) + # Make sure we handle ip changes + if not current_entry or host != urlparse(current_entry["location"]).hostname: _LOGGER.debug("Yeelight discovered with %s", response) self._async_discovered_by_ssdp(response) self._host_capabilities[host] = response @@ -571,7 +576,7 @@ class YeelightDevice: self._bulb_device = bulb self.capabilities = {} self._device_type = None - self._available = False + self._available = True self._initialized = False self._did_first_update = False self._name = None diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index aed2025ab5d..3ad99fa34ac 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -9,8 +9,6 @@ from homeassistant.components.yeelight import ( CONF_MODEL, CONF_NIGHTLIGHT_SWITCH, CONF_NIGHTLIGHT_SWITCH_TYPE, - DATA_CONFIG_ENTRIES, - DATA_DEVICE, DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, STATE_CHANGE_TIME, @@ -57,41 +55,41 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant): ) config_entry.add_to_hass(hass) - mocked_bulb = _mocked_bulb(True) - mocked_bulb.bulb_type = BulbType.WhiteTempMood - mocked_bulb.async_listen = AsyncMock(side_effect=[BulbException, None, None, None]) + mocked_fail_bulb = _mocked_bulb(cannot_connect=True) + mocked_fail_bulb.bulb_type = BulbType.WhiteTempMood + with patch( + f"{MODULE}.AsyncBulb", return_value=mocked_fail_bulb + ), _patch_discovery(): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) + await hass.async_block_till_done() + + # The discovery should update the ip address + assert config_entry.data[CONF_HOST] == IP_ADDRESS + assert config_entry.state is ConfigEntryState.SETUP_RETRY + mocked_bulb = _mocked_bulb() with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( f"yeelight_color_{SHORT_ID}" ) - - type(mocked_bulb).async_get_properties = AsyncMock(None) - - await hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][ - DATA_DEVICE - ].async_update() - await hass.async_block_till_done() - await hass.async_block_till_done() - entity_registry = er.async_get(hass) assert entity_registry.async_get(binary_sensor_entity_id) is not None - with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(): - # The discovery should update the ip address - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) - await hass.async_block_till_done() - assert config_entry.data[CONF_HOST] == IP_ADDRESS - # Make sure we can still reload with the new ip right after we change it with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(): await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + entity_registry = er.async_get(hass) assert entity_registry.async_get(binary_sensor_entity_id) is not None @@ -328,13 +326,21 @@ async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant): ) config_entry.add_to_hass(hass) - mocked_bulb = _mocked_bulb(True) + mocked_bulb = _mocked_bulb(cannot_connect=True) mocked_bulb.bulb_type = BulbType.WhiteTempMood with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery( no_device=True ), _patch_discovery_timeout(), _patch_discovery_interval(): - assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + with patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()), _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(): + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED @@ -401,7 +407,7 @@ async def test_async_listen_error_has_host_with_id(hass: HomeAssistant): ): await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_async_listen_error_has_host_without_id(hass: HomeAssistant): @@ -433,9 +439,16 @@ async def test_async_setup_with_missing_id(hass: HomeAssistant): f"{MODULE}.AsyncBulb", return_value=_mocked_bulb(cannot_connect=True) ): await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.SETUP_RETRY + assert config_entry.data[CONF_ID] == ID - assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.data[CONF_ID] == ID + with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE}.AsyncBulb", return_value=_mocked_bulb() + ): + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED async def test_connection_dropped_resyncs_properties(hass: HomeAssistant):