From 94be43e3e1907cefce22a7a54f1607f29b61fa9f Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 21 Feb 2019 20:29:07 +0100 Subject: [PATCH] Add support for automatic discovery of TP-Link switches, bulbs and dimmers (#18091) * {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...) --- .../components/tplink/.translations/en.json | 15 ++ homeassistant/components/tplink/__init__.py | 154 +++++++++++++++ .../{light/tplink.py => tplink/light.py} | 96 ++++++---- homeassistant/components/tplink/strings.json | 15 ++ .../{switch/tplink.py => tplink/switch.py} | 88 +++++---- homeassistant/config_entries.py | 3 +- requirements_all.txt | 3 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/tplink/__init__.py | 1 + tests/components/tplink/test_init.py | 181 ++++++++++++++++++ 11 files changed, 484 insertions(+), 76 deletions(-) create mode 100644 homeassistant/components/tplink/.translations/en.json create mode 100644 homeassistant/components/tplink/__init__.py rename homeassistant/components/{light/tplink.py => tplink/light.py} (72%) create mode 100644 homeassistant/components/tplink/strings.json rename homeassistant/components/{switch/tplink.py => tplink/switch.py} (59%) create mode 100644 tests/components/tplink/__init__.py create mode 100644 tests/components/tplink/test_init.py diff --git a/homeassistant/components/tplink/.translations/en.json b/homeassistant/components/tplink/.translations/en.json new file mode 100644 index 00000000000..e353c1363ab --- /dev/null +++ b/homeassistant/components/tplink/.translations/en.json @@ -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." + } + } +} diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py new file mode 100644 index 00000000000..bc285150890 --- /dev/null +++ b/homeassistant/components/tplink/__init__.py @@ -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) diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/tplink/light.py similarity index 72% rename from homeassistant/components/light/tplink.py rename to homeassistant/components/tplink/light.py index bd1621a0b35..5d22b1ae60f 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/tplink/light.py @@ -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: diff --git a/homeassistant/components/tplink/strings.json b/homeassistant/components/tplink/strings.json new file mode 100644 index 00000000000..e353c1363ab --- /dev/null +++ b/homeassistant/components/tplink/strings.json @@ -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." + } + } +} diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/tplink/switch.py similarity index 59% rename from homeassistant/components/switch/tplink.py rename to homeassistant/components/tplink/switch.py index 67c8094a1f2..efff0eb4f51 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/tplink/switch.py @@ -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 diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 7c2a3155557..7ff93051c9d 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -172,13 +172,14 @@ FLOWS = [ 'smhi', 'sonos', 'tellduslive', + 'tplink', 'tradfri', 'twilio', 'unifi', 'upnp', 'zha', 'zone', - 'zwave' + 'zwave', ] diff --git a/requirements_all.txt b/requirements_all.txt index 3345af80f09..2f71a3f14cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2f16502ea8..f35d582bcab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 85d9c95aec7..24d081349a0 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -105,6 +105,7 @@ TEST_REQUIREMENTS = ( 'pyunifi', 'pyupnp-async', 'pywebpush', + 'pyHS100', 'regenmaschine', 'restrictedpython', 'rflink', diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py new file mode 100644 index 00000000000..865c6c1d97a --- /dev/null +++ b/tests/components/tplink/__init__.py @@ -0,0 +1 @@ +"""Tests for the TP-Link component.""" diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py new file mode 100644 index 00000000000..1b234428c94 --- /dev/null +++ b/tests/components/tplink/test_init.py @@ -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]