Do not block setup of TP-Link when device unreachable (#53770)

This commit is contained in:
Michael 2021-08-01 23:58:55 +02:00 committed by GitHub
parent 7d1324d66d
commit da1a9bcbf0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 137 additions and 37 deletions

View file

@ -4,6 +4,7 @@ from __future__ import annotations
from datetime import datetime, timedelta
import logging
import time
from typing import Any
from pyHS100.smartdevice import SmartDevice, SmartDeviceException
from pyHS100.smartplug import SmartPlug
@ -22,9 +23,9 @@ from homeassistant.const import (
CONF_STATE,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.dt import utc_from_timestamp
@ -44,6 +45,8 @@ from .const import (
CONF_SWITCH,
COORDINATORS,
PLATFORMS,
UNAVAILABLE_DEVICES,
UNAVAILABLE_RETRY_DELAY,
)
_LOGGER = logging.getLogger(__name__)
@ -96,16 +99,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up TPLink from a config entry."""
config_data = hass.data[DOMAIN].get(ATTR_CONFIG)
if config_data is None and entry.data:
config_data = entry.data
elif config_data is not None:
hass.config_entries.async_update_entry(entry, data=config_data)
device_registry = dr.async_get(hass)
tplink_devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
device_count = len(tplink_devices)
hass_data: dict[str, Any] = hass.data[DOMAIN]
# These will contain the initialized devices
hass.data[DOMAIN][CONF_LIGHT] = []
hass.data[DOMAIN][CONF_SWITCH] = []
lights: list[SmartDevice] = hass.data[DOMAIN][CONF_LIGHT]
switches: list[SmartPlug] = hass.data[DOMAIN][CONF_SWITCH]
hass_data[CONF_LIGHT] = []
hass_data[CONF_SWITCH] = []
hass_data[UNAVAILABLE_DEVICES] = []
lights: list[SmartDevice] = hass_data[CONF_LIGHT]
switches: list[SmartPlug] = hass_data[CONF_SWITCH]
unavailable_devices: list[SmartDevice] = hass_data[UNAVAILABLE_DEVICES]
# Add static devices
static_devices = SmartDevices()
@ -136,22 +146,56 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
", ".join(d.host for d in switches),
)
async def async_retry_devices(self) -> None:
"""Retry unavailable devices."""
unavailable_devices: list[SmartDevice] = hass_data[UNAVAILABLE_DEVICES]
_LOGGER.debug(
"retry during setup unavailable devices: %s",
[d.host for d in unavailable_devices],
)
for device in unavailable_devices:
try:
device.get_sysinfo()
except SmartDeviceException:
continue
_LOGGER.debug(
"at least one device is available again, so reload integration"
)
await hass.config_entries.async_reload(entry.entry_id)
break
# prepare DataUpdateCoordinators
hass.data[DOMAIN][COORDINATORS] = {}
hass_data[COORDINATORS] = {}
for switch in switches:
try:
await hass.async_add_executor_job(switch.get_sysinfo)
except SmartDeviceException as ex:
_LOGGER.debug(ex)
raise ConfigEntryNotReady from ex
except SmartDeviceException:
_LOGGER.warning(
"Device at '%s' not reachable during setup, will retry later",
switch.host,
)
unavailable_devices.append(switch)
continue
hass.data[DOMAIN][COORDINATORS][
hass_data[COORDINATORS][
switch.mac
] = coordinator = SmartPlugDataUpdateCoordinator(hass, switch)
await coordinator.async_config_entry_first_refresh()
if unavailable_devices:
entry.async_on_unload(
async_track_time_interval(
hass, async_retry_devices, UNAVAILABLE_RETRY_DELAY
)
)
unavailable_devices_hosts = [d.host for d in unavailable_devices]
hass_data[CONF_SWITCH] = [
s for s in switches if s.host not in unavailable_devices_hosts
]
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
@ -159,10 +203,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
platforms = [platform for platform in PLATFORMS if hass.data[DOMAIN].get(platform)]
unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass_data: dict[str, Any] = hass.data[DOMAIN]
if unload_ok:
hass.data[DOMAIN].clear()
hass_data.clear()
return unload_ok

View file

@ -5,6 +5,8 @@ import datetime
DOMAIN = "tplink"
COORDINATORS = "coordinators"
UNAVAILABLE_DEVICES = "unavailable_devices"
UNAVAILABLE_RETRY_DELAY = datetime.timedelta(seconds=300)
MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=8)
MAX_DISCOVERY_RETRIES = 4

View file

@ -1,6 +1,6 @@
"""Constants for the TP-Link component tests."""
SMARTPLUGSWITCH_DATA = {
SMARTPLUG_HS110_DATA = {
"sysinfo": {
"sw_ver": "1.0.4 Build 191111 Rel.143500",
"hw_ver": "4.0",
@ -34,6 +34,33 @@ SMARTPLUGSWITCH_DATA = {
"err_code": 0,
},
}
SMARTPLUG_HS100_DATA = {
"sysinfo": {
"sw_ver": "1.0.4 Build 191111 Rel.143500",
"hw_ver": "4.0",
"model": "HS100(EU)",
"deviceId": "4C56447B395BB7A2FAC68C9DFEE2E84163222581",
"oemId": "40F54B43071E9436B6395611E9D91CEA",
"hwId": "A6C77E4FDD238B53D824AC8DA361F043",
"rssi": -24,
"longitude_i": 130793,
"latitude_i": 480582,
"alias": "SmartPlug",
"status": "new",
"mic_type": "IOT.SMARTPLUGSWITCH",
"feature": "TIM:",
"mac": "A9:F4:3D:A4:E3:47",
"updating": 0,
"led_off": 0,
"relay_state": 0,
"on_time": 0,
"active_mode": "none",
"icon_hash": "",
"dev_name": "Smart Wi-Fi Plug",
"next_action": {"type": -1},
"err_code": 0,
}
}
SMARTSTRIPWITCH_DATA = {
"sysinfo": {
"sw_ver": "1.0.4 Build 191111 Rel.143500",

View file

@ -11,6 +11,8 @@ import pytest
from homeassistant import config_entries, data_entry_flow
from homeassistant.components import tplink
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.tplink.common import SmartDevices
from homeassistant.components.tplink.const import (
CONF_DIMMER,
@ -19,16 +21,21 @@ from homeassistant.components.tplink.const import (
CONF_SW_VERSION,
CONF_SWITCH,
COORDINATORS,
UNAVAILABLE_RETRY_DELAY,
)
from homeassistant.components.tplink.sensor import ENERGY_SENSORS
from homeassistant.const import CONF_ALIAS, CONF_DEVICE_ID, CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
from homeassistant.util import slugify
from homeassistant.util import dt, slugify
from tests.common import MockConfigEntry, mock_coro
from tests.components.tplink.consts import SMARTPLUGSWITCH_DATA, SMARTSTRIPWITCH_DATA
from tests.common import MockConfigEntry, async_fire_time_changed, mock_coro
from tests.components.tplink.consts import (
SMARTPLUG_HS100_DATA,
SMARTPLUG_HS110_DATA,
SMARTSTRIPWITCH_DATA,
)
async def test_creating_entry_tries_discover(hass):
@ -220,9 +227,9 @@ async def test_platforms_are_initialized(hass: HomeAssistant):
light = SmartBulb("123.123.123.123")
switch = SmartPlug("321.321.321.321")
switch.get_sysinfo = MagicMock(return_value=SMARTPLUGSWITCH_DATA["sysinfo"])
switch.get_sysinfo = MagicMock(return_value=SMARTPLUG_HS110_DATA["sysinfo"])
switch.get_emeter_realtime = MagicMock(
return_value=EmeterStatus(SMARTPLUGSWITCH_DATA["realtime"])
return_value=EmeterStatus(SMARTPLUG_HS110_DATA["realtime"])
)
switch.get_emeter_daily = MagicMock(
return_value={int(time.strftime("%e")): 1.123}
@ -270,25 +277,22 @@ async def test_smartplug_without_consumption_sensors(hass: HomeAssistant):
), patch(
"homeassistant.components.tplink.light.async_setup_entry",
return_value=mock_coro(True),
), patch(
"homeassistant.components.tplink.switch.async_setup_entry",
return_value=mock_coro(True),
), patch(
"homeassistant.components.tplink.common.SmartPlug.is_dimmable", False
):
switch = SmartPlug("321.321.321.321")
switch.get_sysinfo = MagicMock(return_value=SMARTPLUGSWITCH_DATA["sysinfo"])
switch.get_sysinfo = MagicMock(return_value=SMARTPLUG_HS100_DATA["sysinfo"])
get_static_devices.return_value = SmartDevices([], [switch])
await async_setup_component(hass, tplink.DOMAIN, config)
await hass.async_block_till_done()
for description in ENERGY_SENSORS:
state = hass.states.get(
f"sensor.{switch.alias}_{slugify(description.name)}"
)
assert state is None
entities = hass.states.async_entity_ids(SWITCH_DOMAIN)
assert len(entities) == 1
entities = hass.states.async_entity_ids(SENSOR_DOMAIN)
assert len(entities) == 0
async def test_smartstrip_device(hass: HomeAssistant):
@ -346,8 +350,8 @@ async def test_no_config_creates_no_entry(hass):
assert mock_setup.call_count == 0
async def test_not_ready(hass: HomeAssistant):
"""Test for not ready when configured devices are not available."""
async def test_not_available_at_startup(hass: HomeAssistant):
"""Test when configured devices are not available."""
config = {
tplink.DOMAIN: {
CONF_DISCOVERY: False,
@ -362,9 +366,6 @@ async def test_not_ready(hass: HomeAssistant):
), patch(
"homeassistant.components.tplink.light.async_setup_entry",
return_value=mock_coro(True),
), patch(
"homeassistant.components.tplink.switch.async_setup_entry",
return_value=mock_coro(True),
), patch(
"homeassistant.components.tplink.common.SmartPlug.is_dimmable", False
):
@ -373,13 +374,39 @@ async def test_not_ready(hass: HomeAssistant):
switch.get_sysinfo = MagicMock(side_effect=SmartDeviceException())
get_static_devices.return_value = SmartDevices([], [switch])
# run setup while device unreachable
await async_setup_component(hass, tplink.DOMAIN, config)
await hass.async_block_till_done()
entries = hass.config_entries.async_entries(tplink.DOMAIN)
assert len(entries) == 1
assert entries[0].state is config_entries.ConfigEntryState.SETUP_RETRY
assert entries[0].state is config_entries.ConfigEntryState.LOADED
entities = hass.states.async_entity_ids(SWITCH_DOMAIN)
assert len(entities) == 0
# retrying with still unreachable device
async_fire_time_changed(hass, dt.utcnow() + UNAVAILABLE_RETRY_DELAY)
await hass.async_block_till_done()
entries = hass.config_entries.async_entries(tplink.DOMAIN)
assert len(entries) == 1
assert entries[0].state is config_entries.ConfigEntryState.LOADED
entities = hass.states.async_entity_ids(SWITCH_DOMAIN)
assert len(entities) == 0
# retrying with now reachable device
switch.get_sysinfo = MagicMock(return_value=SMARTPLUG_HS100_DATA["sysinfo"])
async_fire_time_changed(hass, dt.utcnow() + UNAVAILABLE_RETRY_DELAY)
await hass.async_block_till_done()
entries = hass.config_entries.async_entries(tplink.DOMAIN)
assert len(entries) == 1
assert entries[0].state is config_entries.ConfigEntryState.LOADED
entities = hass.states.async_entity_ids(SWITCH_DOMAIN)
assert len(entities) == 1
@pytest.mark.parametrize("platform", ["switch", "light"])
@ -406,9 +433,9 @@ async def test_unload(hass, platform):
light = SmartBulb("123.123.123.123")
switch = SmartPlug("321.321.321.321")
switch.get_sysinfo = MagicMock(return_value=SMARTPLUGSWITCH_DATA["sysinfo"])
switch.get_sysinfo = MagicMock(return_value=SMARTPLUG_HS110_DATA["sysinfo"])
switch.get_emeter_realtime = MagicMock(
return_value=EmeterStatus(SMARTPLUGSWITCH_DATA["realtime"])
return_value=EmeterStatus(SMARTPLUG_HS110_DATA["realtime"])
)
if platform == "light":
get_static_devices.return_value = SmartDevices([light], [])