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...)
2019-02-21 19:29:07 +00:00
|
|
|
"""Tests for the TP-Link component."""
|
2021-09-27 19:11:55 +00:00
|
|
|
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
|
2022-03-22 06:20:40 +00:00
|
|
|
from kasa import (
|
|
|
|
SmartBulb,
|
|
|
|
SmartDevice,
|
|
|
|
SmartDimmer,
|
|
|
|
SmartLightStrip,
|
|
|
|
SmartPlug,
|
|
|
|
SmartStrip,
|
|
|
|
)
|
2021-09-27 19:11:55 +00:00
|
|
|
from kasa.exceptions import SmartDeviceException
|
2021-09-28 16:36:45 +00:00
|
|
|
from kasa.protocol import TPLinkSmartHomeProtocol
|
2021-09-27 19:11:55 +00:00
|
|
|
|
2022-02-06 22:50:44 +00:00
|
|
|
from homeassistant.components.tplink import CONF_HOST
|
|
|
|
from homeassistant.components.tplink.const import DOMAIN
|
|
|
|
from homeassistant.core import HomeAssistant
|
|
|
|
|
|
|
|
from tests.common import MockConfigEntry
|
|
|
|
|
2021-09-27 19:11:55 +00:00
|
|
|
MODULE = "homeassistant.components.tplink"
|
|
|
|
MODULE_CONFIG_FLOW = "homeassistant.components.tplink.config_flow"
|
|
|
|
IP_ADDRESS = "127.0.0.1"
|
|
|
|
ALIAS = "My Bulb"
|
|
|
|
MODEL = "HS100"
|
|
|
|
MAC_ADDRESS = "aa:bb:cc:dd:ee:ff"
|
|
|
|
DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}"
|
|
|
|
|
|
|
|
|
2021-09-28 16:36:45 +00:00
|
|
|
def _mock_protocol() -> TPLinkSmartHomeProtocol:
|
|
|
|
protocol = MagicMock(auto_spec=TPLinkSmartHomeProtocol)
|
|
|
|
protocol.close = AsyncMock()
|
|
|
|
return protocol
|
|
|
|
|
|
|
|
|
2021-09-27 19:11:55 +00:00
|
|
|
def _mocked_bulb() -> SmartBulb:
|
2022-02-21 18:02:11 +00:00
|
|
|
bulb = MagicMock(auto_spec=SmartBulb, name="Mocked bulb")
|
2021-09-27 19:11:55 +00:00
|
|
|
bulb.update = AsyncMock()
|
|
|
|
bulb.mac = MAC_ADDRESS
|
|
|
|
bulb.alias = ALIAS
|
|
|
|
bulb.model = MODEL
|
|
|
|
bulb.host = IP_ADDRESS
|
|
|
|
bulb.brightness = 50
|
|
|
|
bulb.color_temp = 4000
|
|
|
|
bulb.is_color = True
|
|
|
|
bulb.is_strip = False
|
|
|
|
bulb.is_plug = False
|
2021-10-08 03:15:13 +00:00
|
|
|
bulb.is_dimmer = False
|
2022-03-22 06:20:40 +00:00
|
|
|
bulb.is_light_strip = False
|
|
|
|
bulb.has_effects = False
|
|
|
|
bulb.effect = None
|
|
|
|
bulb.effect_list = None
|
2021-09-27 19:11:55 +00:00
|
|
|
bulb.hsv = (10, 30, 5)
|
|
|
|
bulb.device_id = MAC_ADDRESS
|
|
|
|
bulb.valid_temperature_range.min = 4000
|
|
|
|
bulb.valid_temperature_range.max = 9000
|
2022-02-06 22:37:54 +00:00
|
|
|
bulb.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"}
|
2021-09-27 19:11:55 +00:00
|
|
|
bulb.turn_off = AsyncMock()
|
|
|
|
bulb.turn_on = AsyncMock()
|
|
|
|
bulb.set_brightness = AsyncMock()
|
|
|
|
bulb.set_hsv = AsyncMock()
|
|
|
|
bulb.set_color_temp = AsyncMock()
|
2021-09-28 16:36:45 +00:00
|
|
|
bulb.protocol = _mock_protocol()
|
2021-09-27 19:11:55 +00:00
|
|
|
return bulb
|
|
|
|
|
|
|
|
|
2022-03-22 06:20:40 +00:00
|
|
|
class MockedSmartLightStrip(SmartLightStrip):
|
|
|
|
"""Mock a SmartLightStrip."""
|
|
|
|
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
|
|
"""Mock a SmartLightStrip that will pass an isinstance check."""
|
|
|
|
return MagicMock(spec=cls)
|
|
|
|
|
|
|
|
|
|
|
|
def _mocked_smart_light_strip() -> SmartLightStrip:
|
|
|
|
strip = MockedSmartLightStrip()
|
|
|
|
strip.update = AsyncMock()
|
|
|
|
strip.mac = MAC_ADDRESS
|
|
|
|
strip.alias = ALIAS
|
|
|
|
strip.model = MODEL
|
|
|
|
strip.host = IP_ADDRESS
|
|
|
|
strip.brightness = 50
|
|
|
|
strip.color_temp = 4000
|
|
|
|
strip.is_color = True
|
|
|
|
strip.is_strip = False
|
|
|
|
strip.is_plug = False
|
|
|
|
strip.is_dimmer = False
|
|
|
|
strip.is_light_strip = True
|
|
|
|
strip.has_effects = True
|
|
|
|
strip.effect = {"name": "Effect1", "enable": 1}
|
|
|
|
strip.effect_list = ["Effect1", "Effect2"]
|
|
|
|
strip.hsv = (10, 30, 5)
|
|
|
|
strip.device_id = MAC_ADDRESS
|
|
|
|
strip.valid_temperature_range.min = 4000
|
|
|
|
strip.valid_temperature_range.max = 9000
|
|
|
|
strip.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"}
|
|
|
|
strip.turn_off = AsyncMock()
|
|
|
|
strip.turn_on = AsyncMock()
|
|
|
|
strip.set_brightness = AsyncMock()
|
|
|
|
strip.set_hsv = AsyncMock()
|
|
|
|
strip.set_color_temp = AsyncMock()
|
|
|
|
strip.set_effect = AsyncMock()
|
|
|
|
strip.set_custom_effect = AsyncMock()
|
|
|
|
strip.protocol = _mock_protocol()
|
|
|
|
return strip
|
|
|
|
|
|
|
|
|
2021-10-10 07:02:33 +00:00
|
|
|
def _mocked_dimmer() -> SmartDimmer:
|
2022-02-21 18:02:11 +00:00
|
|
|
dimmer = MagicMock(auto_spec=SmartDimmer, name="Mocked dimmer")
|
2021-10-10 07:02:33 +00:00
|
|
|
dimmer.update = AsyncMock()
|
|
|
|
dimmer.mac = MAC_ADDRESS
|
2022-02-21 18:02:11 +00:00
|
|
|
dimmer.alias = "My Dimmer"
|
2021-10-10 07:02:33 +00:00
|
|
|
dimmer.model = MODEL
|
|
|
|
dimmer.host = IP_ADDRESS
|
|
|
|
dimmer.brightness = 50
|
|
|
|
dimmer.color_temp = 4000
|
|
|
|
dimmer.is_color = True
|
|
|
|
dimmer.is_strip = False
|
|
|
|
dimmer.is_plug = False
|
|
|
|
dimmer.is_dimmer = True
|
2022-03-22 06:20:40 +00:00
|
|
|
dimmer.is_light_strip = False
|
|
|
|
dimmer.effect = None
|
|
|
|
dimmer.effect_list = None
|
2021-10-10 07:02:33 +00:00
|
|
|
dimmer.hsv = (10, 30, 5)
|
|
|
|
dimmer.device_id = MAC_ADDRESS
|
|
|
|
dimmer.valid_temperature_range.min = 4000
|
|
|
|
dimmer.valid_temperature_range.max = 9000
|
2022-02-06 22:37:54 +00:00
|
|
|
dimmer.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"}
|
2021-10-10 07:02:33 +00:00
|
|
|
dimmer.turn_off = AsyncMock()
|
|
|
|
dimmer.turn_on = AsyncMock()
|
|
|
|
dimmer.set_brightness = AsyncMock()
|
|
|
|
dimmer.set_hsv = AsyncMock()
|
|
|
|
dimmer.set_color_temp = AsyncMock()
|
2022-02-21 18:02:11 +00:00
|
|
|
dimmer.set_led = AsyncMock()
|
2021-10-10 07:02:33 +00:00
|
|
|
dimmer.protocol = _mock_protocol()
|
|
|
|
return dimmer
|
|
|
|
|
|
|
|
|
2021-09-27 19:11:55 +00:00
|
|
|
def _mocked_plug() -> SmartPlug:
|
2022-02-21 18:02:11 +00:00
|
|
|
plug = MagicMock(auto_spec=SmartPlug, name="Mocked plug")
|
2021-09-27 19:11:55 +00:00
|
|
|
plug.update = AsyncMock()
|
|
|
|
plug.mac = MAC_ADDRESS
|
|
|
|
plug.alias = "My Plug"
|
|
|
|
plug.model = MODEL
|
|
|
|
plug.host = IP_ADDRESS
|
|
|
|
plug.is_light_strip = False
|
|
|
|
plug.is_bulb = False
|
|
|
|
plug.is_dimmer = False
|
|
|
|
plug.is_strip = False
|
|
|
|
plug.is_plug = True
|
|
|
|
plug.device_id = MAC_ADDRESS
|
2022-02-06 22:37:54 +00:00
|
|
|
plug.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"}
|
2021-09-27 19:11:55 +00:00
|
|
|
plug.turn_off = AsyncMock()
|
|
|
|
plug.turn_on = AsyncMock()
|
2021-11-13 23:50:37 +00:00
|
|
|
plug.set_led = AsyncMock()
|
2021-09-28 16:36:45 +00:00
|
|
|
plug.protocol = _mock_protocol()
|
2021-09-27 19:11:55 +00:00
|
|
|
return plug
|
|
|
|
|
|
|
|
|
|
|
|
def _mocked_strip() -> SmartStrip:
|
2022-02-21 18:02:11 +00:00
|
|
|
strip = MagicMock(auto_spec=SmartStrip, name="Mocked strip")
|
2021-09-27 19:11:55 +00:00
|
|
|
strip.update = AsyncMock()
|
|
|
|
strip.mac = MAC_ADDRESS
|
|
|
|
strip.alias = "My Strip"
|
|
|
|
strip.model = MODEL
|
|
|
|
strip.host = IP_ADDRESS
|
|
|
|
strip.is_light_strip = False
|
|
|
|
strip.is_bulb = False
|
|
|
|
strip.is_dimmer = False
|
|
|
|
strip.is_strip = True
|
|
|
|
strip.is_plug = True
|
|
|
|
strip.device_id = MAC_ADDRESS
|
2022-02-06 22:37:54 +00:00
|
|
|
strip.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"}
|
2021-09-27 19:11:55 +00:00
|
|
|
strip.turn_off = AsyncMock()
|
|
|
|
strip.turn_on = AsyncMock()
|
2021-11-13 23:50:37 +00:00
|
|
|
strip.set_led = AsyncMock()
|
2021-09-28 16:36:45 +00:00
|
|
|
strip.protocol = _mock_protocol()
|
2021-09-27 19:11:55 +00:00
|
|
|
plug0 = _mocked_plug()
|
|
|
|
plug0.alias = "Plug0"
|
|
|
|
plug0.device_id = "bb:bb:cc:dd:ee:ff_PLUG0DEVICEID"
|
|
|
|
plug0.mac = "bb:bb:cc:dd:ee:ff"
|
2021-09-28 16:36:45 +00:00
|
|
|
plug0.protocol = _mock_protocol()
|
2021-09-27 19:11:55 +00:00
|
|
|
plug1 = _mocked_plug()
|
|
|
|
plug1.device_id = "cc:bb:cc:dd:ee:ff_PLUG1DEVICEID"
|
|
|
|
plug1.mac = "cc:bb:cc:dd:ee:ff"
|
|
|
|
plug1.alias = "Plug1"
|
2021-09-28 16:36:45 +00:00
|
|
|
plug1.protocol = _mock_protocol()
|
2021-09-27 19:11:55 +00:00
|
|
|
strip.children = [plug0, plug1]
|
|
|
|
return strip
|
|
|
|
|
|
|
|
|
|
|
|
def _patch_discovery(device=None, no_device=False):
|
2021-09-28 14:58:25 +00:00
|
|
|
async def _discovery(*args, **kwargs):
|
2021-09-27 19:11:55 +00:00
|
|
|
if no_device:
|
|
|
|
return {}
|
|
|
|
return {IP_ADDRESS: _mocked_bulb()}
|
|
|
|
|
|
|
|
return patch("homeassistant.components.tplink.Discover.discover", new=_discovery)
|
|
|
|
|
|
|
|
|
|
|
|
def _patch_single_discovery(device=None, no_device=False):
|
|
|
|
async def _discover_single(*_):
|
|
|
|
if no_device:
|
|
|
|
raise SmartDeviceException
|
|
|
|
return device if device else _mocked_bulb()
|
|
|
|
|
|
|
|
return patch(
|
|
|
|
"homeassistant.components.tplink.Discover.discover_single", new=_discover_single
|
|
|
|
)
|
2022-02-06 22:50:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def initialize_config_entry_for_device(
|
|
|
|
hass: HomeAssistant, dev: SmartDevice
|
|
|
|
) -> MockConfigEntry:
|
|
|
|
"""Create a mocked configuration entry for the given device.
|
|
|
|
|
|
|
|
Note, the rest of the tests should probably be converted over to use this
|
|
|
|
instead of repeating the initialization routine for each test separately
|
|
|
|
"""
|
|
|
|
config_entry = MockConfigEntry(
|
|
|
|
title="TP-Link", domain=DOMAIN, unique_id=dev.mac, data={CONF_HOST: dev.host}
|
|
|
|
)
|
|
|
|
config_entry.add_to_hass(hass)
|
|
|
|
|
|
|
|
with _patch_discovery(device=dev), _patch_single_discovery(device=dev):
|
|
|
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
|
|
|
|
return config_entry
|