mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 18:27:09 +00:00
Do not block setup of TP-Link when device unreachable (#53770)
This commit is contained in:
parent
7d1324d66d
commit
da1a9bcbf0
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from pyHS100.smartdevice import SmartDevice, SmartDeviceException
|
from pyHS100.smartdevice import SmartDevice, SmartDeviceException
|
||||||
from pyHS100.smartplug import SmartPlug
|
from pyHS100.smartplug import SmartPlug
|
||||||
@ -22,9 +23,9 @@ from homeassistant.const import (
|
|||||||
CONF_STATE,
|
CONF_STATE,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
import homeassistant.helpers.config_validation as cv
|
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.typing import ConfigType
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
from homeassistant.util.dt import utc_from_timestamp
|
from homeassistant.util.dt import utc_from_timestamp
|
||||||
@ -44,6 +45,8 @@ from .const import (
|
|||||||
CONF_SWITCH,
|
CONF_SWITCH,
|
||||||
COORDINATORS,
|
COORDINATORS,
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
|
UNAVAILABLE_DEVICES,
|
||||||
|
UNAVAILABLE_RETRY_DELAY,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up TPLink from a config entry."""
|
"""Set up TPLink from a config entry."""
|
||||||
config_data = hass.data[DOMAIN].get(ATTR_CONFIG)
|
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)
|
device_registry = dr.async_get(hass)
|
||||||
tplink_devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
|
tplink_devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
|
||||||
device_count = len(tplink_devices)
|
device_count = len(tplink_devices)
|
||||||
|
hass_data: dict[str, Any] = hass.data[DOMAIN]
|
||||||
|
|
||||||
# These will contain the initialized devices
|
# These will contain the initialized devices
|
||||||
hass.data[DOMAIN][CONF_LIGHT] = []
|
hass_data[CONF_LIGHT] = []
|
||||||
hass.data[DOMAIN][CONF_SWITCH] = []
|
hass_data[CONF_SWITCH] = []
|
||||||
lights: list[SmartDevice] = hass.data[DOMAIN][CONF_LIGHT]
|
hass_data[UNAVAILABLE_DEVICES] = []
|
||||||
switches: list[SmartPlug] = hass.data[DOMAIN][CONF_SWITCH]
|
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
|
# Add static devices
|
||||||
static_devices = SmartDevices()
|
static_devices = SmartDevices()
|
||||||
@ -136,22 +146,56 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
", ".join(d.host for d in switches),
|
", ".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
|
# prepare DataUpdateCoordinators
|
||||||
hass.data[DOMAIN][COORDINATORS] = {}
|
hass_data[COORDINATORS] = {}
|
||||||
for switch in switches:
|
for switch in switches:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await hass.async_add_executor_job(switch.get_sysinfo)
|
await hass.async_add_executor_job(switch.get_sysinfo)
|
||||||
except SmartDeviceException as ex:
|
except SmartDeviceException:
|
||||||
_LOGGER.debug(ex)
|
_LOGGER.warning(
|
||||||
raise ConfigEntryNotReady from ex
|
"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
|
switch.mac
|
||||||
] = coordinator = SmartPlugDataUpdateCoordinator(hass, switch)
|
] = coordinator = SmartPlugDataUpdateCoordinator(hass, switch)
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
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)
|
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
return True
|
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:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""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:
|
if unload_ok:
|
||||||
hass.data[DOMAIN].clear()
|
hass_data.clear()
|
||||||
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ import datetime
|
|||||||
|
|
||||||
DOMAIN = "tplink"
|
DOMAIN = "tplink"
|
||||||
COORDINATORS = "coordinators"
|
COORDINATORS = "coordinators"
|
||||||
|
UNAVAILABLE_DEVICES = "unavailable_devices"
|
||||||
|
UNAVAILABLE_RETRY_DELAY = datetime.timedelta(seconds=300)
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=8)
|
MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=8)
|
||||||
MAX_DISCOVERY_RETRIES = 4
|
MAX_DISCOVERY_RETRIES = 4
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""Constants for the TP-Link component tests."""
|
"""Constants for the TP-Link component tests."""
|
||||||
|
|
||||||
SMARTPLUGSWITCH_DATA = {
|
SMARTPLUG_HS110_DATA = {
|
||||||
"sysinfo": {
|
"sysinfo": {
|
||||||
"sw_ver": "1.0.4 Build 191111 Rel.143500",
|
"sw_ver": "1.0.4 Build 191111 Rel.143500",
|
||||||
"hw_ver": "4.0",
|
"hw_ver": "4.0",
|
||||||
@ -34,6 +34,33 @@ SMARTPLUGSWITCH_DATA = {
|
|||||||
"err_code": 0,
|
"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 = {
|
SMARTSTRIPWITCH_DATA = {
|
||||||
"sysinfo": {
|
"sysinfo": {
|
||||||
"sw_ver": "1.0.4 Build 191111 Rel.143500",
|
"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 import config_entries, data_entry_flow
|
||||||
from homeassistant.components import tplink
|
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.common import SmartDevices
|
||||||
from homeassistant.components.tplink.const import (
|
from homeassistant.components.tplink.const import (
|
||||||
CONF_DIMMER,
|
CONF_DIMMER,
|
||||||
@ -19,16 +21,21 @@ from homeassistant.components.tplink.const import (
|
|||||||
CONF_SW_VERSION,
|
CONF_SW_VERSION,
|
||||||
CONF_SWITCH,
|
CONF_SWITCH,
|
||||||
COORDINATORS,
|
COORDINATORS,
|
||||||
|
UNAVAILABLE_RETRY_DELAY,
|
||||||
)
|
)
|
||||||
from homeassistant.components.tplink.sensor import ENERGY_SENSORS
|
from homeassistant.components.tplink.sensor import ENERGY_SENSORS
|
||||||
from homeassistant.const import CONF_ALIAS, CONF_DEVICE_ID, CONF_HOST
|
from homeassistant.const import CONF_ALIAS, CONF_DEVICE_ID, CONF_HOST
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.setup import async_setup_component
|
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.common import MockConfigEntry, async_fire_time_changed, mock_coro
|
||||||
from tests.components.tplink.consts import SMARTPLUGSWITCH_DATA, SMARTSTRIPWITCH_DATA
|
from tests.components.tplink.consts import (
|
||||||
|
SMARTPLUG_HS100_DATA,
|
||||||
|
SMARTPLUG_HS110_DATA,
|
||||||
|
SMARTSTRIPWITCH_DATA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_creating_entry_tries_discover(hass):
|
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")
|
light = SmartBulb("123.123.123.123")
|
||||||
switch = SmartPlug("321.321.321.321")
|
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(
|
switch.get_emeter_realtime = MagicMock(
|
||||||
return_value=EmeterStatus(SMARTPLUGSWITCH_DATA["realtime"])
|
return_value=EmeterStatus(SMARTPLUG_HS110_DATA["realtime"])
|
||||||
)
|
)
|
||||||
switch.get_emeter_daily = MagicMock(
|
switch.get_emeter_daily = MagicMock(
|
||||||
return_value={int(time.strftime("%e")): 1.123}
|
return_value={int(time.strftime("%e")): 1.123}
|
||||||
@ -270,25 +277,22 @@ async def test_smartplug_without_consumption_sensors(hass: HomeAssistant):
|
|||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.tplink.light.async_setup_entry",
|
"homeassistant.components.tplink.light.async_setup_entry",
|
||||||
return_value=mock_coro(True),
|
return_value=mock_coro(True),
|
||||||
), patch(
|
|
||||||
"homeassistant.components.tplink.switch.async_setup_entry",
|
|
||||||
return_value=mock_coro(True),
|
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.tplink.common.SmartPlug.is_dimmable", False
|
"homeassistant.components.tplink.common.SmartPlug.is_dimmable", False
|
||||||
):
|
):
|
||||||
|
|
||||||
switch = SmartPlug("321.321.321.321")
|
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])
|
get_static_devices.return_value = SmartDevices([], [switch])
|
||||||
|
|
||||||
await async_setup_component(hass, tplink.DOMAIN, config)
|
await async_setup_component(hass, tplink.DOMAIN, config)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
for description in ENERGY_SENSORS:
|
entities = hass.states.async_entity_ids(SWITCH_DOMAIN)
|
||||||
state = hass.states.get(
|
assert len(entities) == 1
|
||||||
f"sensor.{switch.alias}_{slugify(description.name)}"
|
|
||||||
)
|
entities = hass.states.async_entity_ids(SENSOR_DOMAIN)
|
||||||
assert state is None
|
assert len(entities) == 0
|
||||||
|
|
||||||
|
|
||||||
async def test_smartstrip_device(hass: HomeAssistant):
|
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
|
assert mock_setup.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
async def test_not_ready(hass: HomeAssistant):
|
async def test_not_available_at_startup(hass: HomeAssistant):
|
||||||
"""Test for not ready when configured devices are not available."""
|
"""Test when configured devices are not available."""
|
||||||
config = {
|
config = {
|
||||||
tplink.DOMAIN: {
|
tplink.DOMAIN: {
|
||||||
CONF_DISCOVERY: False,
|
CONF_DISCOVERY: False,
|
||||||
@ -362,9 +366,6 @@ async def test_not_ready(hass: HomeAssistant):
|
|||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.tplink.light.async_setup_entry",
|
"homeassistant.components.tplink.light.async_setup_entry",
|
||||||
return_value=mock_coro(True),
|
return_value=mock_coro(True),
|
||||||
), patch(
|
|
||||||
"homeassistant.components.tplink.switch.async_setup_entry",
|
|
||||||
return_value=mock_coro(True),
|
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.tplink.common.SmartPlug.is_dimmable", False
|
"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())
|
switch.get_sysinfo = MagicMock(side_effect=SmartDeviceException())
|
||||||
get_static_devices.return_value = SmartDevices([], [switch])
|
get_static_devices.return_value = SmartDevices([], [switch])
|
||||||
|
|
||||||
|
# run setup while device unreachable
|
||||||
await async_setup_component(hass, tplink.DOMAIN, config)
|
await async_setup_component(hass, tplink.DOMAIN, config)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
entries = hass.config_entries.async_entries(tplink.DOMAIN)
|
entries = hass.config_entries.async_entries(tplink.DOMAIN)
|
||||||
|
|
||||||
assert len(entries) == 1
|
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"])
|
@pytest.mark.parametrize("platform", ["switch", "light"])
|
||||||
@ -406,9 +433,9 @@ async def test_unload(hass, platform):
|
|||||||
|
|
||||||
light = SmartBulb("123.123.123.123")
|
light = SmartBulb("123.123.123.123")
|
||||||
switch = SmartPlug("321.321.321.321")
|
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(
|
switch.get_emeter_realtime = MagicMock(
|
||||||
return_value=EmeterStatus(SMARTPLUGSWITCH_DATA["realtime"])
|
return_value=EmeterStatus(SMARTPLUG_HS110_DATA["realtime"])
|
||||||
)
|
)
|
||||||
if platform == "light":
|
if platform == "light":
|
||||||
get_static_devices.return_value = SmartDevices([light], [])
|
get_static_devices.return_value = SmartDevices([light], [])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user