Do not block setup of TP-Link when device unreachable (#53770)
This commit is contained in:
parent
7d1324d66d
commit
da1a9bcbf0
4 changed files with 137 additions and 37 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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], [])
|
||||
|
|
Loading…
Add table
Reference in a new issue