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
|
|
|
|
|
2021-10-10 07:02:33 +00:00
|
|
|
from kasa import SmartBulb, SmartDimmer, 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
|
|
|
|
|
|
|
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:
|
|
|
|
bulb = MagicMock(auto_spec=SmartBulb)
|
|
|
|
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
|
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
|
|
|
|
bulb.hw_info = {"sw_ver": "1.0.0"}
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2021-10-10 07:02:33 +00:00
|
|
|
def _mocked_dimmer() -> SmartDimmer:
|
|
|
|
dimmer = MagicMock(auto_spec=SmartDimmer)
|
|
|
|
dimmer.update = AsyncMock()
|
|
|
|
dimmer.mac = MAC_ADDRESS
|
|
|
|
dimmer.alias = ALIAS
|
|
|
|
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
|
|
|
|
dimmer.hsv = (10, 30, 5)
|
|
|
|
dimmer.device_id = MAC_ADDRESS
|
|
|
|
dimmer.valid_temperature_range.min = 4000
|
|
|
|
dimmer.valid_temperature_range.max = 9000
|
|
|
|
dimmer.hw_info = {"sw_ver": "1.0.0"}
|
|
|
|
dimmer.turn_off = AsyncMock()
|
|
|
|
dimmer.turn_on = AsyncMock()
|
|
|
|
dimmer.set_brightness = AsyncMock()
|
|
|
|
dimmer.set_hsv = AsyncMock()
|
|
|
|
dimmer.set_color_temp = AsyncMock()
|
|
|
|
dimmer.protocol = _mock_protocol()
|
|
|
|
return dimmer
|
|
|
|
|
|
|
|
|
2021-09-27 19:11:55 +00:00
|
|
|
def _mocked_plug() -> SmartPlug:
|
|
|
|
plug = MagicMock(auto_spec=SmartPlug)
|
|
|
|
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
|
|
|
|
plug.hw_info = {"sw_ver": "1.0.0"}
|
|
|
|
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:
|
|
|
|
strip = MagicMock(auto_spec=SmartStrip)
|
|
|
|
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
|
|
|
|
strip.hw_info = {"sw_ver": "1.0.0"}
|
|
|
|
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
|
|
|
|
)
|