Add support for automatic discovery of TP-Link switches, bulbs and dimmers ()

* {switch,light}.tplink: use deviceid as unique id, fetch name from the device during initialization

* raise PlatformNotReady when no device is available

* Use mac instead of deviceid

* remove name option as obsolete

* Add support for configuration flow / integration

Allows activating automatic discovery of supported devices from the configuration

* Fix linting, update requirements_all.txt

* start cleaning up tplink component based on feedback

* add device info, improve config handling

* Allow overriding detected devices via configuration file

* Update requirements.txt

* Remove debug logging

* make hound happy

* Avoid I/O during init and simplify the code, remove remains of leds_on

* Fix issues based on feedback, use consistent quotation marks for device info

* add async_setup_platform emiting a deprecation warning

* Avoid blocking the I/O, check for None on features

* handle some Martin's comments, schema-validation is still missing

* use async_create_task instead of async_add_job, let core validate the schema

* simplify configuration handling by storing the configuration data separately from initialized instances

* add default values to schema, make hound happy

* with defaults set by schema, simplify the checks. add async_unload_entry

* Use constant for data structure access

* REWORD add a short note about async_unload_entry

* handle feedback from Martin, config_data is checked against Noneness

* use pop to remove the domain on unload

* First steps to add tests for the new tplink component

* embed platforms under the component directory

* Fix tests by mocking the pyhs100 internals

* Fix linting

* Test against multiple instances of devices, tidy up

* (hopefully) final linting round

* Add pyHS100 to test requirements

* log always the warnings occured during an update to make them easy to see

* revert back the warning behavior (requirement for silver level in IQS)

* Unload only when an entry is being loaded and add tests for that

Thanks @MartinHjelmare for pointing this out!

* Fix linting

* Bump the upstream lib, fixes most prominently the HSV setting on bulbs

* Test unloading for all platforms, clear the data storage instead of popping it out, making it possible to reconfigure after removal without restarting hass first

* Use class variables instead of instance variables for bulb states, required for HS220

* Use new-style format string

* Fix indenting, uppercase the mock constant

* Run black on test_init, hopefully that will finally fix the weird formatting (pycharm, pylint and hound seems to have different opinions...)
pull/21287/head
Teemu R 2019-02-21 20:29:07 +01:00 committed by Martin Hjelmare
parent c637bad1eb
commit 94be43e3e1
11 changed files with 484 additions and 76 deletions

View File

@ -0,0 +1,15 @@
{
"config": {
"title": "TP-Link Smart Home",
"step": {
"confirm": {
"title": "TP-Link Smart Home",
"description": "Do you want to setup TP-Link smart devices?"
}
},
"abort": {
"single_instance_allowed": "Only a single configuration is necessary.",
"no_devices_found": "No TP-Link devices found on the network."
}
}
}

View File

@ -0,0 +1,154 @@
"""Component to embed TP-Link smart home devices."""
import logging
import voluptuous as vol
from homeassistant.const import CONF_HOST
from homeassistant import config_entries
from homeassistant.helpers import config_entry_flow
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'tplink'
TPLINK_HOST_SCHEMA = vol.Schema({
vol.Required(CONF_HOST): cv.string
})
CONF_LIGHT = 'light'
CONF_SWITCH = 'switch'
CONF_DISCOVERY = 'discovery'
ATTR_CONFIG = 'config'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional('light', default=[]): vol.All(cv.ensure_list,
[TPLINK_HOST_SCHEMA]),
vol.Optional('switch', default=[]): vol.All(cv.ensure_list,
[TPLINK_HOST_SCHEMA]),
vol.Optional('discovery', default=True): cv.boolean,
}),
}, extra=vol.ALLOW_EXTRA)
REQUIREMENTS = ['pyHS100==0.3.4']
async def _async_has_devices(hass):
"""Return if there are devices that can be discovered."""
from pyHS100 import Discover
def discover():
devs = Discover.discover()
return devs
return await hass.async_add_executor_job(discover)
async def async_setup(hass, config):
"""Set up the TP-Link component."""
conf = config.get(DOMAIN)
hass.data[DOMAIN] = {}
hass.data[DOMAIN][ATTR_CONFIG] = conf
if conf is not None:
hass.async_create_task(hass.config_entries.flow.async_init(
DOMAIN, context={'source': config_entries.SOURCE_IMPORT}))
return True
async def async_setup_entry(hass, config_entry):
"""Set up TPLink from a config entry."""
from pyHS100 import SmartBulb, SmartPlug, SmartDeviceException
devices = {}
config_data = hass.data[DOMAIN].get(ATTR_CONFIG)
# These will contain the initialized devices
lights = hass.data[DOMAIN][CONF_LIGHT] = []
switches = hass.data[DOMAIN][CONF_SWITCH] = []
# If discovery is defined and not disabled, discover devices
# If initialized from configure integrations, there's no config
# so we default here to True
if config_data is None or config_data[CONF_DISCOVERY]:
devs = await _async_has_devices(hass)
_LOGGER.info("Discovered %s TP-Link smart home device(s)", len(devs))
devices.update(devs)
def _device_for_type(host, type_):
dev = None
if type_ == CONF_LIGHT:
dev = SmartBulb(host)
elif type_ == CONF_SWITCH:
dev = SmartPlug(host)
return dev
# When arriving from configure integrations, we have no config data.
if config_data is not None:
for type_ in [CONF_LIGHT, CONF_SWITCH]:
for entry in config_data[type_]:
try:
host = entry['host']
dev = _device_for_type(host, type_)
devices[host] = dev
_LOGGER.debug("Succesfully added %s %s: %s",
type_, host, dev)
except SmartDeviceException as ex:
_LOGGER.error("Unable to initialize %s %s: %s",
type_, host, ex)
# This is necessary to avoid I/O blocking on is_dimmable
def _fill_device_lists():
for dev in devices.values():
if isinstance(dev, SmartPlug):
if dev.is_dimmable: # Dimmers act as lights
lights.append(dev)
else:
switches.append(dev)
elif isinstance(dev, SmartBulb):
lights.append(dev)
else:
_LOGGER.error("Unknown smart device type: %s", type(dev))
# Avoid blocking on is_dimmable
await hass.async_add_executor_job(_fill_device_lists)
forward_setup = hass.config_entries.async_forward_entry_setup
if lights:
_LOGGER.debug("Got %s lights: %s", len(lights), lights)
hass.async_create_task(forward_setup(config_entry, 'light'))
if switches:
_LOGGER.debug("Got %s switches: %s", len(switches), switches)
hass.async_create_task(forward_setup(config_entry, 'switch'))
return True
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
forward_unload = hass.config_entries.async_forward_entry_unload
remove_lights = remove_switches = False
if hass.data[DOMAIN][CONF_LIGHT]:
remove_lights = await forward_unload(entry, 'light')
if hass.data[DOMAIN][CONF_SWITCH]:
remove_switches = await forward_unload(entry, 'switch')
if remove_lights or remove_switches:
hass.data[DOMAIN].clear()
return True
# We were not able to unload the platforms, either because there
# were none or one of the forward_unloads failed.
return False
config_entry_flow.register_discovery_flow(DOMAIN,
'TP-Link Smart Home',
_async_has_devices,
config_entries.CONN_CLASS_LOCAL_POLL)

View File

@ -7,19 +7,20 @@ https://home-assistant.io/components/light.tplink/
import logging
import time
import voluptuous as vol
from homeassistant.const import (CONF_HOST, CONF_NAME)
from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP, SUPPORT_COLOR, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
SUPPORT_COLOR_TEMP, SUPPORT_COLOR)
from homeassistant.util.color import \
color_temperature_mired_to_kelvin as mired_to_kelvin
from homeassistant.util.color import (
color_temperature_kelvin_to_mired as kelvin_to_mired)
import homeassistant.helpers.device_registry as dr
from homeassistant.components.tplink import (DOMAIN as TPLINK_DOMAIN,
CONF_LIGHT)
REQUIREMENTS = ['pyHS100==0.3.4']
DEPENDENCIES = ['tplink']
PARALLEL_UPDATES = 0
_LOGGER = logging.getLogger(__name__)
@ -27,20 +28,25 @@ ATTR_CURRENT_POWER_W = 'current_power_w'
ATTR_DAILY_ENERGY_KWH = 'daily_energy_kwh'
ATTR_MONTHLY_ENERGY_KWH = 'monthly_energy_kwh'
DEFAULT_NAME = 'TP-Link Light'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
})
def async_setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the platform.
Deprecated.
"""
_LOGGER.warning('Loading as a platform is deprecated, '
'convert to use the tplink component.')
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Initialise pyLB100 SmartBulb."""
from pyHS100 import SmartBulb
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
add_entities([TPLinkSmartBulb(SmartBulb(host), name)], True)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up discovered switches."""
devs = []
for dev in hass.data[TPLINK_DOMAIN][CONF_LIGHT]:
devs.append(TPLinkSmartBulb(dev))
async_add_entities(devs, True)
return True
def brightness_to_percentage(byt):
@ -56,25 +62,42 @@ def brightness_from_percentage(percent):
class TPLinkSmartBulb(Light):
"""Representation of a TPLink Smart Bulb."""
# F821: https://github.com/PyCQA/pyflakes/issues/373
def __init__(self, smartbulb: 'SmartBulb', name) -> None: # noqa: F821
def __init__(self, smartbulb) -> None:
"""Initialize the bulb."""
self.smartbulb = smartbulb
self._name = name
self._sysinfo = None
self._state = None
self._available = True
self._available = False
self._color_temp = None
self._brightness = None
self._hs = None
self._supported_features = 0
self._supported_features = None
self._min_mireds = None
self._max_mireds = None
self._emeter_params = {}
@property
def unique_id(self):
"""Return a unique ID."""
return self._sysinfo["mac"]
@property
def name(self):
"""Return the name of the Smart Bulb, if any."""
return self._name
"""Return the name of the Smart Bulb."""
return self._sysinfo["alias"]
@property
def device_info(self):
"""Return information about the device."""
return {
"name": self.name,
"model": self._sysinfo["model"],
"manufacturer": 'TP-Link',
"connections": {
(dr.CONNECTION_NETWORK_MAC, self._sysinfo["mac"])
},
"sw_version": self._sysinfo["sw_ver"],
}
@property
def available(self) -> bool:
@ -88,7 +111,8 @@ class TPLinkSmartBulb(Light):
def turn_on(self, **kwargs):
"""Turn the light on."""
self.smartbulb.state = self.smartbulb.BULB_STATE_ON
from pyHS100 import SmartBulb
self.smartbulb.state = SmartBulb.BULB_STATE_ON
if ATTR_COLOR_TEMP in kwargs:
self.smartbulb.color_temp = \
@ -105,7 +129,8 @@ class TPLinkSmartBulb(Light):
def turn_off(self, **kwargs):
"""Turn the light off."""
self.smartbulb.state = self.smartbulb.BULB_STATE_OFF
from pyHS100 import SmartBulb
self.smartbulb.state = SmartBulb.BULB_STATE_OFF
@property
def min_mireds(self):
@ -139,17 +164,13 @@ class TPLinkSmartBulb(Light):
def update(self):
"""Update the TP-Link Bulb's state."""
from pyHS100 import SmartDeviceException
from pyHS100 import SmartDeviceException, SmartBulb
try:
if self._supported_features == 0:
if self._supported_features is None:
self.get_features()
self._state = (
self.smartbulb.state == self.smartbulb.BULB_STATE_ON)
# Pull the name from the device if a name was not specified
if self._name == DEFAULT_NAME:
self._name = self.smartbulb.alias
self.smartbulb.state == SmartBulb.BULB_STATE_ON)
if self._supported_features & SUPPORT_BRIGHTNESS:
self._brightness = brightness_from_percentage(
@ -185,9 +206,9 @@ class TPLinkSmartBulb(Light):
except (SmartDeviceException, OSError) as ex:
if self._available:
_LOGGER.warning(
"Could not read state for %s: %s", self._name, ex)
self._available = False
_LOGGER.warning("Could not read state for %s: %s",
self.smartbulb.host, ex)
self._available = False
@property
def supported_features(self):
@ -196,6 +217,9 @@ class TPLinkSmartBulb(Light):
def get_features(self):
"""Determine all supported features in one go."""
self._sysinfo = self.smartbulb.sys_info
self._supported_features = 0
if self.smartbulb.is_dimmable:
self._supported_features += SUPPORT_BRIGHTNESS
if self.smartbulb.is_variable_color_temp:

View File

@ -0,0 +1,15 @@
{
"config": {
"title": "TP-Link Smart Home",
"step": {
"confirm": {
"title": "TP-Link Smart Home",
"description": "Do you want to setup TP-Link smart devices?"
}
},
"abort": {
"single_instance_allowed": "Only a single configuration is necessary.",
"no_devices_found": "No TP-Link devices found on the network."
}
}
}

View File

@ -7,58 +7,77 @@ https://home-assistant.io/components/switch.tplink/
import logging
import time
import voluptuous as vol
from homeassistant.components.switch import (
SwitchDevice, PLATFORM_SCHEMA, ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH)
from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_VOLTAGE)
import homeassistant.helpers.config_validation as cv
SwitchDevice, ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH)
from homeassistant.components.tplink import (DOMAIN as TPLINK_DOMAIN,
CONF_SWITCH)
from homeassistant.const import ATTR_VOLTAGE
import homeassistant.helpers.device_registry as dr
REQUIREMENTS = ['pyHS100==0.3.4']
DEPENDENCIES = ['tplink']
PARALLEL_UPDATES = 0
_LOGGER = logging.getLogger(__name__)
ATTR_TOTAL_ENERGY_KWH = 'total_energy_kwh'
ATTR_CURRENT_A = 'current_a'
CONF_LEDS = 'enable_leds'
DEFAULT_NAME = 'TP-Link Switch'
def async_setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the platform.
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_LEDS): cv.boolean,
})
Deprecated.
"""
_LOGGER.warning('Loading as a platform is deprecated, '
'convert to use the tplink component.')
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the TPLink switch platform."""
from pyHS100 import SmartPlug
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
leds_on = config.get(CONF_LEDS)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up discovered switches."""
devs = []
for dev in hass.data[TPLINK_DOMAIN][CONF_SWITCH]:
devs.append(SmartPlugSwitch(dev))
add_entities([SmartPlugSwitch(SmartPlug(host), name, leds_on)], True)
async_add_entities(devs, True)
return True
class SmartPlugSwitch(SwitchDevice):
"""Representation of a TPLink Smart Plug switch."""
def __init__(self, smartplug, name, leds_on):
def __init__(self, smartplug):
"""Initialize the switch."""
self.smartplug = smartplug
self._name = name
self._leds_on = leds_on
self._sysinfo = None
self._state = None
self._available = True
self._available = False
# Set up emeter cache
self._emeter_params = {}
@property
def unique_id(self):
"""Return a unique ID."""
return self._sysinfo["mac"]
@property
def name(self):
"""Return the name of the Smart Plug, if any."""
return self._name
"""Return the name of the Smart Plug."""
return self._sysinfo["alias"]
@property
def device_info(self):
"""Return information about the device."""
return {
"name": self.name,
"model": self._sysinfo["model"],
"manufacturer": 'TP-Link',
"connections": {
(dr.CONNECTION_NETWORK_MAC, self._sysinfo["mac"])
},
"sw_version": self._sysinfo["sw_ver"],
}
@property
def available(self) -> bool:
@ -87,17 +106,12 @@ class SmartPlugSwitch(SwitchDevice):
"""Update the TP-Link switch's state."""
from pyHS100 import SmartDeviceException
try:
if not self._sysinfo:
self._sysinfo = self.smartplug.sys_info
self._state = self.smartplug.state == \
self.smartplug.SWITCH_STATE_ON
if self._leds_on is not None:
self.smartplug.led = self._leds_on
self._leds_on = None
# Pull the name from the device if a name was not specified
if self._name == DEFAULT_NAME:
self._name = self.smartplug.alias
if self.smartplug.has_emeter:
emeter_readings = self.smartplug.get_emeter_realtime()
@ -123,6 +137,6 @@ class SmartPlugSwitch(SwitchDevice):
except (SmartDeviceException, OSError) as ex:
if self._available:
_LOGGER.warning(
"Could not read state for %s: %s", self.name, ex)
self._available = False
_LOGGER.warning("Could not read state for %s: %s",
self.smartplug.host, ex)
self._available = False

View File

@ -172,13 +172,14 @@ FLOWS = [
'smhi',
'sonos',
'tellduslive',
'tplink',
'tradfri',
'twilio',
'unifi',
'upnp',
'zha',
'zone',
'zwave'
'zwave',
]

View File

@ -892,8 +892,7 @@ py17track==2.1.1
# homeassistant.components.hdmi_cec
pyCEC==0.4.13
# homeassistant.components.light.tplink
# homeassistant.components.switch.tplink
# homeassistant.components.tplink
pyHS100==0.3.4
# homeassistant.components.air_quality.norway_air

View File

@ -180,6 +180,9 @@ pushbullet.py==0.11.0
# homeassistant.components.canary
py-canary==0.5.0
# homeassistant.components.tplink
pyHS100==0.3.4
# homeassistant.components.media_player.blackbird
pyblackbird==0.5

View File

@ -105,6 +105,7 @@ TEST_REQUIREMENTS = (
'pyunifi',
'pyupnp-async',
'pywebpush',
'pyHS100',
'regenmaschine',
'restrictedpython',
'rflink',

View File

@ -0,0 +1 @@
"""Tests for the TP-Link component."""

View File

@ -0,0 +1,181 @@
"""Tests for the TP-Link component."""
from unittest.mock import patch
import pytest
from homeassistant import config_entries, data_entry_flow
from homeassistant.components import tplink
from homeassistant.setup import async_setup_component
from pyHS100 import SmartPlug, SmartBulb
from tests.common import MockDependency, MockConfigEntry, mock_coro
MOCK_PYHS100 = MockDependency("pyHS100")
async def test_creating_entry_tries_discover(hass):
"""Test setting up does discovery."""
with MOCK_PYHS100, patch(
"homeassistant.components.tplink.async_setup_entry",
return_value=mock_coro(True),
) as mock_setup, patch(
"pyHS100.Discover.discover", return_value={"host": 1234}
):
result = await hass.config_entries.flow.async_init(
tplink.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
# Confirmation form
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
async def test_configuring_tplink_causes_discovery(hass):
"""Test that specifying empty config does discovery."""
with MOCK_PYHS100, patch("pyHS100.Discover.discover") as discover:
discover.return_value = {"host": 1234}
await async_setup_component(hass, tplink.DOMAIN, {"tplink": {}})
await hass.async_block_till_done()
assert len(discover.mock_calls) == 1
@pytest.mark.parametrize(
"name,cls,platform",
[
("pyHS100.SmartPlug", SmartPlug, "switch"),
("pyHS100.SmartBulb", SmartBulb, "light"),
],
)
@pytest.mark.parametrize("count", [1, 2, 3])
async def test_configuring_device_types(hass, name, cls, platform, count):
"""Test that light or switch platform list is filled correctly."""
with patch("pyHS100.Discover.discover") as discover, patch(
"pyHS100.SmartDevice._query_helper"
):
discovery_data = {
"123.123.123.{}".format(c): cls("123.123.123.123")
for c in range(count)
}
discover.return_value = discovery_data
await async_setup_component(hass, tplink.DOMAIN, {"tplink": {}})
await hass.async_block_till_done()
assert len(discover.mock_calls) == 1
assert len(hass.data[tplink.DOMAIN][platform]) == count
async def test_is_dimmable(hass):
"""Test that is_dimmable switches are correctly added as lights."""
with patch("pyHS100.Discover.discover") as discover, patch(
"homeassistant.components.tplink.light.async_setup_entry",
return_value=mock_coro(True),
) as setup, patch("pyHS100.SmartDevice._query_helper"), patch(
"pyHS100.SmartPlug.is_dimmable", True
):
dimmable_switch = SmartPlug("123.123.123.123")
discover.return_value = {"host": dimmable_switch}
await async_setup_component(hass, tplink.DOMAIN, {"tplink": {}})
await hass.async_block_till_done()
assert len(discover.mock_calls) == 1
assert len(setup.mock_calls) == 1
assert len(hass.data[tplink.DOMAIN]["light"]) == 1
assert len(hass.data[tplink.DOMAIN]["switch"]) == 0
async def test_configuring_discovery_disabled(hass):
"""Test that discover does not get called when disabled."""
with MOCK_PYHS100, patch(
"homeassistant.components.tplink.async_setup_entry",
return_value=mock_coro(True),
) as mock_setup, patch(
"pyHS100.Discover.discover", return_value=[]
) as discover:
await async_setup_component(
hass,
tplink.DOMAIN,
{tplink.DOMAIN: {tplink.CONF_DISCOVERY: False}},
)
await hass.async_block_till_done()
assert len(discover.mock_calls) == 0
assert len(mock_setup.mock_calls) == 1
async def test_platforms_are_initialized(hass):
"""Test that platforms are initialized per configuration array."""
config = {
"tplink": {
"discovery": False,
"light": [{"host": "123.123.123.123"}],
"switch": [{"host": "321.321.321.321"}],
}
}
with patch("pyHS100.Discover.discover") as discover, patch(
"pyHS100.SmartDevice._query_helper"
), patch(
"homeassistant.components.tplink.light.async_setup_entry",
return_value=mock_coro(True),
) as light_setup, patch(
"homeassistant.components.tplink.switch.async_setup_entry",
return_value=mock_coro(True),
) as switch_setup, patch(
"pyHS100.SmartPlug.is_dimmable", False
):
# patching is_dimmable is necessray to avoid misdetection as light.
await async_setup_component(hass, tplink.DOMAIN, config)
await hass.async_block_till_done()
assert len(discover.mock_calls) == 0
assert len(light_setup.mock_calls) == 1
assert len(switch_setup.mock_calls) == 1
async def test_no_config_creates_no_entry(hass):
"""Test for when there is no tplink in config."""
with MOCK_PYHS100, patch(
"homeassistant.components.tplink.async_setup_entry",
return_value=mock_coro(True),
) as mock_setup:
await async_setup_component(hass, tplink.DOMAIN, {})
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 0
@pytest.mark.parametrize("platform", ["switch", "light"])
async def test_unload(hass, platform):
"""Test that the async_unload_entry works."""
# As we have currently no configuration, we just to pass the domain here.
entry = MockConfigEntry(domain=tplink.DOMAIN)
entry.add_to_hass(hass)
with patch("pyHS100.SmartDevice._query_helper"), patch(
"homeassistant.components.tplink.{}"
".async_setup_entry".format(platform),
return_value=mock_coro(True),
) as light_setup:
config = {
"tplink": {
platform: [{"host": "123.123.123.123"}],
"discovery": False,
}
}
assert await async_setup_component(hass, tplink.DOMAIN, config)
await hass.async_block_till_done()
assert len(light_setup.mock_calls) == 1
assert tplink.DOMAIN in hass.data
assert await tplink.async_unload_entry(hass, entry)
assert not hass.data[tplink.DOMAIN]