Fix yeelight connection when bulb stops responding to SSDP (#57138)

pull/57135/head
J. Nick Koston 2021-10-05 10:41:56 -10:00 committed by GitHub
parent e22407ba16
commit eba7cad33f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 53 additions and 35 deletions

View File

@ -181,6 +181,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
DATA_CUSTOM_EFFECTS: conf.get(CONF_CUSTOM_EFFECTS, {}), DATA_CUSTOM_EFFECTS: conf.get(CONF_CUSTOM_EFFECTS, {}),
DATA_CONFIG_ENTRIES: {}, 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 # Import manually configured devices
for host, device_config in config.get(DOMAIN, {}).get(CONF_DEVICES, {}).items(): 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: try:
device = await _async_get_device(hass, entry.data[CONF_HOST], entry) device = await _async_get_device(hass, entry.data[CONF_HOST], entry)
except BULB_EXCEPTIONS as ex: except BULB_EXCEPTIONS as ex:
# If CONF_ID is not valid we cannot fallback to discovery # Always retry later since bulbs can stop responding to SSDP
# so we must retry by raising ConfigEntryNotReady # sometimes even though they are online. If it has changed
if not entry.data.get(CONF_ID): # IP we will update it via discovery to the config flow
raise ConfigEntryNotReady from ex raise ConfigEntryNotReady from ex
# Otherwise fall through to discovery
else: else:
# Since device is passed this cannot throw an exception anymore # Since device is passed this cannot throw an exception anymore
await _async_initialize(hass, entry, entry.data[CONF_HOST], device=device) 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: except BULB_EXCEPTIONS:
_LOGGER.exception("Failed to connect to bulb at %s", host) _LOGGER.exception("Failed to connect to bulb at %s", host)
# discovery
scanner = YeelightScanner.async_get(hass) scanner = YeelightScanner.async_get(hass)
await scanner.async_register_callback(entry.data[CONF_ID], _async_from_discovery) await scanner.async_register_callback(entry.data[CONF_ID], _async_from_discovery)
return True return True
@ -501,7 +504,9 @@ class YeelightScanner:
_LOGGER.debug("Discovered via SSDP: %s", response) _LOGGER.debug("Discovered via SSDP: %s", response)
unique_id = response["id"] unique_id = response["id"]
host = urlparse(response["location"]).hostname 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) _LOGGER.debug("Yeelight discovered with %s", response)
self._async_discovered_by_ssdp(response) self._async_discovered_by_ssdp(response)
self._host_capabilities[host] = response self._host_capabilities[host] = response
@ -571,7 +576,7 @@ class YeelightDevice:
self._bulb_device = bulb self._bulb_device = bulb
self.capabilities = {} self.capabilities = {}
self._device_type = None self._device_type = None
self._available = False self._available = True
self._initialized = False self._initialized = False
self._did_first_update = False self._did_first_update = False
self._name = None self._name = None

View File

@ -9,8 +9,6 @@ from homeassistant.components.yeelight import (
CONF_MODEL, CONF_MODEL,
CONF_NIGHTLIGHT_SWITCH, CONF_NIGHTLIGHT_SWITCH,
CONF_NIGHTLIGHT_SWITCH_TYPE, CONF_NIGHTLIGHT_SWITCH_TYPE,
DATA_CONFIG_ENTRIES,
DATA_DEVICE,
DOMAIN, DOMAIN,
NIGHTLIGHT_SWITCH_TYPE_LIGHT, NIGHTLIGHT_SWITCH_TYPE_LIGHT,
STATE_CHANGE_TIME, STATE_CHANGE_TIME,
@ -57,41 +55,41 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant):
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
mocked_bulb = _mocked_bulb(True) mocked_fail_bulb = _mocked_bulb(cannot_connect=True)
mocked_bulb.bulb_type = BulbType.WhiteTempMood mocked_fail_bulb.bulb_type = BulbType.WhiteTempMood
mocked_bulb.async_listen = AsyncMock(side_effect=[BulbException, None, None, None]) 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(): with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery():
assert await hass.config_entries.async_setup(config_entry.entry_id) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10))
await hass.async_block_till_done()
await hass.async_block_till_done() await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format(
f"yeelight_color_{SHORT_ID}" 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) entity_registry = er.async_get(hass)
assert entity_registry.async_get(binary_sensor_entity_id) is not None 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 # 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(): with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery():
await hass.config_entries.async_reload(config_entry.entry_id) await hass.config_entries.async_reload(config_entry.entry_id)
await hass.async_block_till_done() 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 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) config_entry.add_to_hass(hass)
mocked_bulb = _mocked_bulb(True) mocked_bulb = _mocked_bulb(cannot_connect=True)
mocked_bulb.bulb_type = BulbType.WhiteTempMood mocked_bulb.bulb_type = BulbType.WhiteTempMood
with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery( with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(
no_device=True no_device=True
), _patch_discovery_timeout(), _patch_discovery_interval(): ), _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() await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED 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) 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): async def test_async_listen_error_has_host_without_id(hass: HomeAssistant):
@ -433,10 +439,17 @@ async def test_async_setup_with_missing_id(hass: HomeAssistant):
f"{MODULE}.AsyncBulb", return_value=_mocked_bulb(cannot_connect=True) f"{MODULE}.AsyncBulb", return_value=_mocked_bulb(cannot_connect=True)
): ):
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.LOADED assert config_entry.state is ConfigEntryState.SETUP_RETRY
assert config_entry.data[CONF_ID] == ID 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): async def test_connection_dropped_resyncs_properties(hass: HomeAssistant):
"""Test handling a connection drop results in a property resync.""" """Test handling a connection drop results in a property resync."""