Merge pull request #24636 from home-assistant/rc

0.94.4
pull/24637/head 0.94.4
Paulus Schoutsen 2019-06-19 16:36:04 -07:00 committed by GitHub
commit d85ae5dcae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 576 additions and 143 deletions

View File

@ -56,6 +56,7 @@ class EsphomeFlowHandler(config_entries.ConfigFlow):
self.context['title_placeholders'] = { self.context['title_placeholders'] = {
'name': self._name 'name': self._name
} }
self.context['name'] = self._name
# Only show authentication step if device uses password # Only show authentication step if device uses password
if device_info.uses_password: if device_info.uses_password:
@ -98,9 +99,11 @@ class EsphomeFlowHandler(config_entries.ConfigFlow):
already_configured = data.device_info.name == node_name already_configured = data.device_info.name == node_name
if already_configured: if already_configured:
return self.async_abort( return self.async_abort(reason='already_configured')
reason='already_configured'
) for flow in self._async_in_progress():
if flow['context']['name'] == node_name:
return self.async_abort(reason='already_configured')
return await self._async_authenticate_or_add(user_input={ return await self._async_authenticate_or_add(user_input={
'host': address, 'host': address,

View File

@ -170,6 +170,7 @@ class Sun(Entity):
utc_point_in_time, 'dusk', PHASE_ASTRONOMICAL_TWILIGHT) utc_point_in_time, 'dusk', PHASE_ASTRONOMICAL_TWILIGHT)
self.next_midnight = self._check_event( self.next_midnight = self._check_event(
utc_point_in_time, 'solar_midnight', None) utc_point_in_time, 'solar_midnight', None)
self.location.solar_depression = 'civil'
# if the event was solar midday or midnight, phase will now # if the event was solar midday or midnight, phase will now
# be None. Solar noon doesn't always happen when the sun is # be None. Solar noon doesn't always happen when the sun is

View File

@ -6,28 +6,43 @@ import voluptuous as vol
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from homeassistant import config_entries from homeassistant import config_entries
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from .config_flow import async_get_devices from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from .const import DOMAIN
from .common import (
async_discover_devices,
get_static_devices,
ATTR_CONFIG,
CONF_DIMMER,
CONF_DISCOVERY,
CONF_LIGHT,
CONF_SWITCH,
SmartDevices
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'tplink'
TPLINK_HOST_SCHEMA = vol.Schema({ TPLINK_HOST_SCHEMA = vol.Schema({
vol.Required(CONF_HOST): cv.string vol.Required(CONF_HOST): cv.string
}) })
CONF_LIGHT = 'light'
CONF_SWITCH = 'switch'
CONF_DISCOVERY = 'discovery'
ATTR_CONFIG = 'config'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Optional('light', default=[]): vol.All(cv.ensure_list, vol.Optional(CONF_LIGHT, default=[]): vol.All(
[TPLINK_HOST_SCHEMA]), cv.ensure_list,
vol.Optional('switch', default=[]): vol.All(cv.ensure_list, [TPLINK_HOST_SCHEMA]
[TPLINK_HOST_SCHEMA]), ),
vol.Optional('discovery', default=True): cv.boolean, vol.Optional(CONF_SWITCH, default=[]): vol.All(
cv.ensure_list,
[TPLINK_HOST_SCHEMA]
),
vol.Optional(CONF_DIMMER, default=[]): vol.All(
cv.ensure_list,
[TPLINK_HOST_SCHEMA]
),
vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -46,76 +61,45 @@ async def async_setup(hass, config):
return True return True
async def async_setup_entry(hass, config_entry): async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigType):
"""Set up TPLink from a config entry.""" """Set up TPLink from a config entry."""
from pyHS100 import SmartBulb, SmartPlug, SmartDeviceException
devices = {}
config_data = hass.data[DOMAIN].get(ATTR_CONFIG) config_data = hass.data[DOMAIN].get(ATTR_CONFIG)
# These will contain the initialized devices # These will contain the initialized devices
lights = hass.data[DOMAIN][CONF_LIGHT] = [] lights = hass.data[DOMAIN][CONF_LIGHT] = []
switches = hass.data[DOMAIN][CONF_SWITCH] = [] switches = hass.data[DOMAIN][CONF_SWITCH] = []
# If discovery is defined and not disabled, discover devices # Add static devices
# If initialized from configure integrations, there's no config static_devices = SmartDevices()
# so we default here to True
if config_data is None or config_data[CONF_DISCOVERY]:
devs = await async_get_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: if config_data is not None:
for type_ in [CONF_LIGHT, CONF_SWITCH]: static_devices = get_static_devices(
for entry in config_data[type_]: config_data,
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 lights.extend(static_devices.lights)
def _fill_device_lists(): switches.extend(static_devices.switches)
for dev in devices.values():
if isinstance(dev, SmartPlug):
try:
if dev.is_dimmable: # Dimmers act as lights
lights.append(dev)
else:
switches.append(dev)
except SmartDeviceException as ex:
_LOGGER.error("Unable to connect to device %s: %s",
dev.host, ex)
elif isinstance(dev, SmartBulb): # Add discovered devices
lights.append(dev) if config_data is None or config_data[CONF_DISCOVERY]:
else: discovered_devices = await async_discover_devices(hass, static_devices)
_LOGGER.error("Unknown smart device type: %s", type(dev))
# Avoid blocking on is_dimmable lights.extend(discovered_devices.lights)
await hass.async_add_executor_job(_fill_device_lists) switches.extend(discovered_devices.switches)
forward_setup = hass.config_entries.async_forward_entry_setup forward_setup = hass.config_entries.async_forward_entry_setup
if lights: if lights:
_LOGGER.debug("Got %s lights: %s", len(lights), lights) _LOGGER.debug(
"Got %s lights: %s",
len(lights),
", ".join([d.host for d in lights])
)
hass.async_create_task(forward_setup(config_entry, 'light')) hass.async_create_task(forward_setup(config_entry, 'light'))
if switches: if switches:
_LOGGER.debug("Got %s switches: %s", len(switches), switches) _LOGGER.debug(
"Got %s switches: %s",
len(switches),
", ".join([d.host for d in switches])
)
hass.async_create_task(forward_setup(config_entry, 'switch')) hass.async_create_task(forward_setup(config_entry, 'switch'))
return True return True

View File

@ -0,0 +1,202 @@
"""Common code for tplink."""
import asyncio
import logging
from datetime import timedelta
from typing import Any, Callable, List
from pyHS100 import (
SmartBulb,
SmartDevice,
SmartPlug,
SmartDeviceException
)
from homeassistant.helpers.typing import HomeAssistantType
_LOGGER = logging.getLogger(__name__)
ATTR_CONFIG = 'config'
CONF_DIMMER = 'dimmer'
CONF_DISCOVERY = 'discovery'
CONF_LIGHT = 'light'
CONF_SWITCH = 'switch'
class SmartDevices:
"""Hold different kinds of devices."""
def __init__(
self,
lights: List[SmartDevice] = None,
switches: List[SmartDevice] = None
):
"""Constructor."""
self._lights = lights or []
self._switches = switches or []
@property
def lights(self):
"""Get the lights."""
return self._lights
@property
def switches(self):
"""Get the switches."""
return self._switches
def has_device_with_host(self, host):
"""Check if a devices exists with a specific host."""
for device in self.lights + self.switches:
if device.host == host:
return True
return False
async def async_get_discoverable_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_discover_devices(
hass: HomeAssistantType,
existing_devices: SmartDevices
) -> SmartDevices:
"""Get devices through discovery."""
_LOGGER.debug("Discovering devices")
devices = await async_get_discoverable_devices(hass)
_LOGGER.info(
"Discovered %s TP-Link smart home device(s)",
len(devices)
)
lights = []
switches = []
def process_devices():
for dev in devices.values():
# If this device already exists, ignore dynamic setup.
if existing_devices.has_device_with_host(dev.host):
continue
if isinstance(dev, SmartPlug):
try:
if dev.is_dimmable: # Dimmers act as lights
lights.append(dev)
else:
switches.append(dev)
except SmartDeviceException as ex:
_LOGGER.error("Unable to connect to device %s: %s",
dev.host, ex)
elif isinstance(dev, SmartBulb):
lights.append(dev)
else:
_LOGGER.error("Unknown smart device type: %s", type(dev))
await hass.async_add_executor_job(process_devices)
return SmartDevices(lights, switches)
def get_static_devices(config_data) -> SmartDevices:
"""Get statically defined devices in the config."""
_LOGGER.debug("Getting static devices")
lights = []
switches = []
for type_ in [CONF_LIGHT, CONF_SWITCH, CONF_DIMMER]:
for entry in config_data[type_]:
host = entry['host']
if type_ == CONF_LIGHT:
lights.append(SmartBulb(host))
elif type_ == CONF_SWITCH:
switches.append(SmartPlug(host))
# Dimmers need to be defined as smart plugs to work correctly.
elif type_ == CONF_DIMMER:
lights.append(SmartPlug(host))
return SmartDevices(
lights,
switches
)
async def async_add_entities_retry(
hass: HomeAssistantType,
async_add_entities: Callable[[List[Any], bool], None],
objects: List[Any],
callback: Callable[[Any, Callable], None],
interval: timedelta = timedelta(seconds=60)
):
"""
Add entities now and retry later if issues are encountered.
If the callback throws an exception or returns false, that
object will try again a while later.
This is useful for devices that are not online when hass starts.
:param hass:
:param async_add_entities: The callback provided to a
platform's async_setup.
:param objects: The objects to create as entities.
:param callback: The callback that will perform the add.
:param interval: THe time between attempts to add.
:return: A callback to cancel the retries.
"""
add_objects = objects.copy()
is_cancelled = False
def cancel_interval_callback():
nonlocal is_cancelled
is_cancelled = True
async def process_objects_loop(delay: int):
if is_cancelled:
return
await process_objects()
if not add_objects:
return
await asyncio.sleep(delay)
hass.async_create_task(process_objects_loop(delay))
async def process_objects(*args):
# Process each object.
for add_object in list(add_objects):
# Call the individual item callback.
try:
_LOGGER.debug(
"Attempting to add object of type %s",
type(add_object)
)
result = await hass.async_add_job(
callback,
add_object,
async_add_entities
)
except SmartDeviceException as ex:
_LOGGER.debug(
str(ex)
)
result = False
if result is True or result is None:
_LOGGER.debug("Added object.")
add_objects.remove(add_object)
else:
_LOGGER.debug("Failed to add object, will try again later")
await process_objects_loop(interval.seconds)
return cancel_interval_callback

View File

@ -2,19 +2,10 @@
from homeassistant.helpers import config_entry_flow from homeassistant.helpers import config_entry_flow
from homeassistant import config_entries from homeassistant import config_entries
from .const import DOMAIN from .const import DOMAIN
from .common import async_get_discoverable_devices
async def async_get_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)
config_entry_flow.register_discovery_flow(DOMAIN, config_entry_flow.register_discovery_flow(DOMAIN,
'TP-Link Smart Home', 'TP-Link Smart Home',
async_get_devices, async_get_discoverable_devices,
config_entries.CONN_CLASS_LOCAL_POLL) config_entries.CONN_CLASS_LOCAL_POLL)

View File

@ -2,15 +2,19 @@
import logging import logging
import time import time
from pyHS100 import SmartBulb, SmartDeviceException
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light) SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light)
import homeassistant.helpers.device_registry as dr import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.color import ( from homeassistant.util.color import (
color_temperature_kelvin_to_mired as kelvin_to_mired, color_temperature_kelvin_to_mired as kelvin_to_mired,
color_temperature_mired_to_kelvin as mired_to_kelvin) color_temperature_mired_to_kelvin as mired_to_kelvin)
from . import CONF_LIGHT, DOMAIN as TPLINK_DOMAIN from . import CONF_LIGHT, DOMAIN as TPLINK_DOMAIN
from .common import async_add_entities_retry
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@ -31,17 +35,35 @@ async def async_setup_platform(hass, config, add_entities,
'convert to use the tplink component.') 'convert to use the tplink component.')
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(
"""Set up discovered switches.""" hass: HomeAssistantType,
devs = [] config_entry,
for dev in hass.data[TPLINK_DOMAIN][CONF_LIGHT]: async_add_entities
devs.append(TPLinkSmartBulb(dev)) ):
"""Set up switches."""
async_add_entities(devs, True) await async_add_entities_retry(
hass,
async_add_entities,
hass.data[TPLINK_DOMAIN][CONF_LIGHT],
add_entity
)
return True return True
def add_entity(device: SmartBulb, async_add_entities):
"""Check if device is online and add the entity."""
# Attempt to get the sysinfo. If it fails, it will raise an
# exception that is caught by async_add_entities_retry which
# will try again later.
device.get_sysinfo()
async_add_entities(
[TPLinkSmartBulb(device)],
update_before_add=True
)
def brightness_to_percentage(byt): def brightness_to_percentage(byt):
"""Convert brightness from absolute 0..255 to percentage.""" """Convert brightness from absolute 0..255 to percentage."""
return int((byt*100.0)/255.0) return int((byt*100.0)/255.0)
@ -55,7 +77,7 @@ def brightness_from_percentage(percent):
class TPLinkSmartBulb(Light): class TPLinkSmartBulb(Light):
"""Representation of a TPLink Smart Bulb.""" """Representation of a TPLink Smart Bulb."""
def __init__(self, smartbulb) -> None: def __init__(self, smartbulb: SmartBulb) -> None:
"""Initialize the bulb.""" """Initialize the bulb."""
self.smartbulb = smartbulb self.smartbulb = smartbulb
self._sysinfo = None self._sysinfo = None
@ -69,25 +91,29 @@ class TPLinkSmartBulb(Light):
self._max_mireds = None self._max_mireds = None
self._emeter_params = {} self._emeter_params = {}
self._mac = None
self._alias = None
self._model = None
@property @property
def unique_id(self): def unique_id(self):
"""Return a unique ID.""" """Return a unique ID."""
return self._sysinfo["mac"] return self._mac
@property @property
def name(self): def name(self):
"""Return the name of the Smart Bulb.""" """Return the name of the Smart Bulb."""
return self._sysinfo["alias"] return self._alias
@property @property
def device_info(self): def device_info(self):
"""Return information about the device.""" """Return information about the device."""
return { return {
"name": self.name, "name": self._alias,
"model": self._sysinfo["model"], "model": self._model,
"manufacturer": 'TP-Link', "manufacturer": 'TP-Link',
"connections": { "connections": {
(dr.CONNECTION_NETWORK_MAC, self._sysinfo["mac"]) (dr.CONNECTION_NETWORK_MAC, self._mac)
}, },
"sw_version": self._sysinfo["sw_ver"], "sw_version": self._sysinfo["sw_ver"],
} }
@ -104,7 +130,6 @@ class TPLinkSmartBulb(Light):
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the light on.""" """Turn the light on."""
from pyHS100 import SmartBulb
self.smartbulb.state = SmartBulb.BULB_STATE_ON self.smartbulb.state = SmartBulb.BULB_STATE_ON
if ATTR_COLOR_TEMP in kwargs: if ATTR_COLOR_TEMP in kwargs:
@ -122,7 +147,6 @@ class TPLinkSmartBulb(Light):
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the light off.""" """Turn the light off."""
from pyHS100 import SmartBulb
self.smartbulb.state = SmartBulb.BULB_STATE_OFF self.smartbulb.state = SmartBulb.BULB_STATE_OFF
@property @property
@ -157,7 +181,6 @@ class TPLinkSmartBulb(Light):
def update(self): def update(self):
"""Update the TP-Link Bulb's state.""" """Update the TP-Link Bulb's state."""
from pyHS100 import SmartDeviceException, SmartBulb
try: try:
if self._supported_features is None: if self._supported_features is None:
self.get_features() self.get_features()
@ -212,6 +235,9 @@ class TPLinkSmartBulb(Light):
"""Determine all supported features in one go.""" """Determine all supported features in one go."""
self._sysinfo = self.smartbulb.sys_info self._sysinfo = self.smartbulb.sys_info
self._supported_features = 0 self._supported_features = 0
self._mac = self.smartbulb.mac
self._alias = self.smartbulb.alias
self._model = self.smartbulb.model
if self.smartbulb.is_dimmable: if self.smartbulb.is_dimmable:
self._supported_features += SUPPORT_BRIGHTNESS self._supported_features += SUPPORT_BRIGHTNESS

View File

@ -2,12 +2,16 @@
import logging import logging
import time import time
from pyHS100 import SmartDeviceException, SmartPlug
from homeassistant.components.switch import ( from homeassistant.components.switch import (
ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH, SwitchDevice) ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH, SwitchDevice)
from homeassistant.const import ATTR_VOLTAGE from homeassistant.const import ATTR_VOLTAGE
import homeassistant.helpers.device_registry as dr import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.typing import HomeAssistantType
from . import CONF_SWITCH, DOMAIN as TPLINK_DOMAIN from . import CONF_SWITCH, DOMAIN as TPLINK_DOMAIN
from .common import async_add_entities_retry
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@ -27,13 +31,31 @@ async def async_setup_platform(hass, config, add_entities,
'convert to use the tplink component.') 'convert to use the tplink component.')
async def async_setup_entry(hass, config_entry, async_add_entities): def add_entity(device: SmartPlug, async_add_entities):
"""Set up discovered switches.""" """Check if device is online and add the entity."""
devs = [] # Attempt to get the sysinfo. If it fails, it will raise an
for dev in hass.data[TPLINK_DOMAIN][CONF_SWITCH]: # exception that is caught by async_add_entities_retry which
devs.append(SmartPlugSwitch(dev)) # will try again later.
device.get_sysinfo()
async_add_entities(devs, True) async_add_entities(
[SmartPlugSwitch(device)],
update_before_add=True
)
async def async_setup_entry(
hass: HomeAssistantType,
config_entry,
async_add_entities
):
"""Set up switches."""
await async_add_entities_retry(
hass,
async_add_entities,
hass.data[TPLINK_DOMAIN][CONF_SWITCH],
add_entity
)
return True return True
@ -41,7 +63,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class SmartPlugSwitch(SwitchDevice): class SmartPlugSwitch(SwitchDevice):
"""Representation of a TPLink Smart Plug switch.""" """Representation of a TPLink Smart Plug switch."""
def __init__(self, smartplug): def __init__(self, smartplug: SmartPlug):
"""Initialize the switch.""" """Initialize the switch."""
self.smartplug = smartplug self.smartplug = smartplug
self._sysinfo = None self._sysinfo = None
@ -50,25 +72,29 @@ class SmartPlugSwitch(SwitchDevice):
# Set up emeter cache # Set up emeter cache
self._emeter_params = {} self._emeter_params = {}
self._mac = None
self._alias = None
self._model = None
@property @property
def unique_id(self): def unique_id(self):
"""Return a unique ID.""" """Return a unique ID."""
return self._sysinfo["mac"] return self._mac
@property @property
def name(self): def name(self):
"""Return the name of the Smart Plug.""" """Return the name of the Smart Plug."""
return self._sysinfo["alias"] return self._alias
@property @property
def device_info(self): def device_info(self):
"""Return information about the device.""" """Return information about the device."""
return { return {
"name": self.name, "name": self._alias,
"model": self._sysinfo["model"], "model": self._model,
"manufacturer": 'TP-Link', "manufacturer": 'TP-Link',
"connections": { "connections": {
(dr.CONNECTION_NETWORK_MAC, self._sysinfo["mac"]) (dr.CONNECTION_NETWORK_MAC, self._mac)
}, },
"sw_version": self._sysinfo["sw_ver"], "sw_version": self._sysinfo["sw_ver"],
} }
@ -98,10 +124,12 @@ class SmartPlugSwitch(SwitchDevice):
def update(self): def update(self):
"""Update the TP-Link switch's state.""" """Update the TP-Link switch's state."""
from pyHS100 import SmartDeviceException
try: try:
if not self._sysinfo: if not self._sysinfo:
self._sysinfo = self.smartplug.sys_info self._sysinfo = self.smartplug.sys_info
self._mac = self.smartplug.mac
self._alias = self.smartplug.alias
self._model = self.smartplug.model
self._state = self.smartplug.state == \ self._state = self.smartplug.state == \
self.smartplug.SWITCH_STATE_ON self.smartplug.SWITCH_STATE_ON

View File

@ -2,7 +2,7 @@
"""Constants used by Home Assistant components.""" """Constants used by Home Assistant components."""
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 94 MINOR_VERSION = 94
PATCH_VERSION = '3' PATCH_VERSION = '4'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5, 3) REQUIRED_PYTHON_VER = (3, 5, 3)

View File

@ -281,3 +281,30 @@ async def test_discovery_already_configured_name(hass, mock_client):
result = await flow.async_step_zeroconf(user_input=service_info) result = await flow.async_step_zeroconf(user_input=service_info)
assert result['type'] == 'abort' assert result['type'] == 'abort'
assert result['reason'] == 'already_configured' assert result['reason'] == 'already_configured'
async def test_discovery_duplicate_data(hass, mock_client):
"""Test discovery aborts if same mDNS packet arrives."""
service_info = {
'host': '192.168.43.183',
'port': 6053,
'hostname': 'test8266.local.',
'properties': {
"address": "test8266.local"
}
}
mock_client.device_info.return_value = mock_coro(
MockDeviceInfo(False, "test8266"))
result = await hass.config_entries.flow.async_init(
'esphome', data=service_info, context={'source': 'zeroconf'}
)
assert result['type'] == 'form'
assert result['step_id'] == 'discovery_confirm'
result = await hass.config_entries.flow.async_init(
'esphome', data=service_info, context={'source': 'zeroconf'}
)
assert result['type'] == 'abort'
assert result['reason'] == 'already_configured'

View File

@ -0,0 +1,97 @@
"""Common code tests."""
from datetime import timedelta
from unittest.mock import MagicMock
from pyHS100 import SmartDeviceException
from homeassistant.components.tplink.common import async_add_entities_retry
from homeassistant.helpers.typing import HomeAssistantType
async def test_async_add_entities_retry(
hass: HomeAssistantType
):
"""Test interval callback."""
async_add_entities_callback = MagicMock()
# The objects that will be passed to async_add_entities_callback.
objects = [
"Object 1",
"Object 2",
"Object 3",
"Object 4",
]
# For each call to async_add_entities_callback, the following side effects
# will be triggered in order. This set of side effects accuratley simulates
# 3 attempts to add all entities while also handling several return types.
# To help understand what's going on, a comment exists describing what the
# object list looks like throughout the iterations.
callback_side_effects = [
# OB1, OB2, OB3, OB4
False,
False,
True, # Object 3
False,
# OB1, OB2, OB4
True, # Object 1
SmartDeviceException("My error"),
False,
# OB2, OB4
True, # Object 2
True, # Object 4
]
callback = MagicMock(side_effect=callback_side_effects)
await async_add_entities_retry(
hass,
async_add_entities_callback,
objects,
callback,
interval=timedelta(milliseconds=100)
)
await hass.async_block_till_done()
assert callback.call_count == len(callback_side_effects)
async def test_async_add_entities_retry_cancel(
hass: HomeAssistantType
):
"""Test interval callback."""
async_add_entities_callback = MagicMock()
callback_side_effects = [
False,
False,
True, # Object 1
False,
True, # Object 2
SmartDeviceException("My error"),
False,
True, # Object 3
True, # Object 4
]
callback = MagicMock(side_effect=callback_side_effects)
objects = [
"Object 1",
"Object 2",
"Object 3",
"Object 4",
]
cancel = await async_add_entities_retry(
hass,
async_add_entities_callback,
objects,
callback,
interval=timedelta(milliseconds=100)
)
cancel()
await hass.async_block_till_done()
assert callback.call_count == 4

View File

@ -1,12 +1,20 @@
"""Tests for the TP-Link component.""" """Tests for the TP-Link component."""
from unittest.mock import patch from typing import Dict, Any
from unittest.mock import MagicMock, patch
import pytest import pytest
from pyHS100 import SmartPlug, SmartBulb, SmartDevice, SmartDeviceException
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.tplink.common import (
CONF_DISCOVERY,
CONF_DIMMER,
CONF_LIGHT,
CONF_SWITCH,
)
from homeassistant.const import CONF_HOST
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from pyHS100 import SmartPlug, SmartBulb
from tests.common import MockDependency, MockConfigEntry, mock_coro from tests.common import MockDependency, MockConfigEntry, mock_coro
MOCK_PYHS100 = MockDependency("pyHS100") MOCK_PYHS100 = MockDependency("pyHS100")
@ -15,8 +23,8 @@ MOCK_PYHS100 = MockDependency("pyHS100")
async def test_creating_entry_tries_discover(hass): async def test_creating_entry_tries_discover(hass):
"""Test setting up does discovery.""" """Test setting up does discovery."""
with MOCK_PYHS100, patch( with MOCK_PYHS100, patch(
"homeassistant.components.tplink.async_setup_entry", "homeassistant.components.tplink.async_setup_entry",
return_value=mock_coro(True), return_value=mock_coro(True),
) as mock_setup, patch( ) as mock_setup, patch(
"pyHS100.Discover.discover", return_value={"host": 1234} "pyHS100.Discover.discover", return_value={"host": 1234}
): ):
@ -41,7 +49,7 @@ async def test_configuring_tplink_causes_discovery(hass):
"""Test that specifying empty config does discovery.""" """Test that specifying empty config does discovery."""
with MOCK_PYHS100, patch("pyHS100.Discover.discover") as discover: with MOCK_PYHS100, patch("pyHS100.Discover.discover") as discover:
discover.return_value = {"host": 1234} discover.return_value = {"host": 1234}
await async_setup_component(hass, tplink.DOMAIN, {"tplink": {}}) await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(discover.mock_calls) == 1 assert len(discover.mock_calls) == 1
@ -58,45 +66,111 @@ async def test_configuring_tplink_causes_discovery(hass):
async def test_configuring_device_types(hass, name, cls, platform, count): async def test_configuring_device_types(hass, name, cls, platform, count):
"""Test that light or switch platform list is filled correctly.""" """Test that light or switch platform list is filled correctly."""
with patch("pyHS100.Discover.discover") as discover, patch( with patch("pyHS100.Discover.discover") as discover, patch(
"pyHS100.SmartDevice._query_helper" "pyHS100.SmartDevice._query_helper"
): ):
discovery_data = { discovery_data = {
"123.123.123.{}".format(c): cls("123.123.123.123") "123.123.123.{}".format(c): cls("123.123.123.123")
for c in range(count) for c in range(count)
} }
discover.return_value = discovery_data discover.return_value = discovery_data
await async_setup_component(hass, tplink.DOMAIN, {"tplink": {}}) await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(discover.mock_calls) == 1 assert len(discover.mock_calls) == 1
assert len(hass.data[tplink.DOMAIN][platform]) == count assert len(hass.data[tplink.DOMAIN][platform]) == count
class UnknownSmartDevice(SmartDevice):
"""Dummy class for testing."""
@property
def has_emeter(self) -> bool:
"""Do nothing."""
pass
def turn_off(self) -> None:
"""Do nothing."""
pass
def turn_on(self) -> None:
"""Do nothing."""
pass
@property
def is_on(self) -> bool:
"""Do nothing."""
pass
@property
def state_information(self) -> Dict[str, Any]:
"""Do nothing."""
pass
async def test_configuring_devices_from_multiple_sources(hass):
"""Test static and discover devices are not duplicated."""
with patch("pyHS100.Discover.discover") as discover, patch(
"pyHS100.SmartDevice._query_helper"
):
discover_device_fail = SmartPlug("123.123.123.123")
discover_device_fail.get_sysinfo = MagicMock(
side_effect=SmartDeviceException()
)
discover.return_value = {
"123.123.123.1": SmartBulb("123.123.123.1"),
"123.123.123.2": SmartPlug("123.123.123.2"),
"123.123.123.3": SmartBulb("123.123.123.3"),
"123.123.123.4": SmartPlug("123.123.123.4"),
"123.123.123.123": discover_device_fail,
"123.123.123.124": UnknownSmartDevice("123.123.123.124")
}
await async_setup_component(hass, tplink.DOMAIN, {
tplink.DOMAIN: {
CONF_LIGHT: [
{CONF_HOST: "123.123.123.1"},
],
CONF_SWITCH: [
{CONF_HOST: "123.123.123.2"},
],
CONF_DIMMER: [
{CONF_HOST: "123.123.123.22"},
],
}
})
await hass.async_block_till_done()
assert len(discover.mock_calls) == 1
assert len(hass.data[tplink.DOMAIN][CONF_LIGHT]) == 3
assert len(hass.data[tplink.DOMAIN][CONF_SWITCH]) == 2
async def test_is_dimmable(hass): async def test_is_dimmable(hass):
"""Test that is_dimmable switches are correctly added as lights.""" """Test that is_dimmable switches are correctly added as lights."""
with patch("pyHS100.Discover.discover") as discover, patch( with patch("pyHS100.Discover.discover") as discover, 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),
) as setup, patch("pyHS100.SmartDevice._query_helper"), patch( ) as setup, patch("pyHS100.SmartDevice._query_helper"), patch(
"pyHS100.SmartPlug.is_dimmable", True "pyHS100.SmartPlug.is_dimmable", True
): ):
dimmable_switch = SmartPlug("123.123.123.123") dimmable_switch = SmartPlug("123.123.123.123")
discover.return_value = {"host": dimmable_switch} discover.return_value = {"host": dimmable_switch}
await async_setup_component(hass, tplink.DOMAIN, {"tplink": {}}) await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(discover.mock_calls) == 1 assert len(discover.mock_calls) == 1
assert len(setup.mock_calls) == 1 assert len(setup.mock_calls) == 1
assert len(hass.data[tplink.DOMAIN]["light"]) == 1 assert len(hass.data[tplink.DOMAIN][CONF_LIGHT]) == 1
assert len(hass.data[tplink.DOMAIN]["switch"]) == 0 assert not hass.data[tplink.DOMAIN][CONF_SWITCH]
async def test_configuring_discovery_disabled(hass): async def test_configuring_discovery_disabled(hass):
"""Test that discover does not get called when disabled.""" """Test that discover does not get called when disabled."""
with MOCK_PYHS100, patch( with MOCK_PYHS100, patch(
"homeassistant.components.tplink.async_setup_entry", "homeassistant.components.tplink.async_setup_entry",
return_value=mock_coro(True), return_value=mock_coro(True),
) as mock_setup, patch( ) as mock_setup, patch(
"pyHS100.Discover.discover", return_value=[] "pyHS100.Discover.discover", return_value=[]
) as discover: ) as discover:
@ -107,22 +181,22 @@ async def test_configuring_discovery_disabled(hass):
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(discover.mock_calls) == 0 assert discover.call_count == 0
assert len(mock_setup.mock_calls) == 1 assert mock_setup.call_count == 1
async def test_platforms_are_initialized(hass): async def test_platforms_are_initialized(hass):
"""Test that platforms are initialized per configuration array.""" """Test that platforms are initialized per configuration array."""
config = { config = {
"tplink": { tplink.DOMAIN: {
"discovery": False, CONF_DISCOVERY: False,
"light": [{"host": "123.123.123.123"}], CONF_LIGHT: [{CONF_HOST: "123.123.123.123"}],
"switch": [{"host": "321.321.321.321"}], CONF_SWITCH: [{CONF_HOST: "321.321.321.321"}],
} }
} }
with patch("pyHS100.Discover.discover") as discover, patch( with patch("pyHS100.Discover.discover") as discover, patch(
"pyHS100.SmartDevice._query_helper" "pyHS100.SmartDevice._query_helper"
), 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),
@ -136,21 +210,21 @@ async def test_platforms_are_initialized(hass):
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()
assert len(discover.mock_calls) == 0 assert discover.call_count == 0
assert len(light_setup.mock_calls) == 1 assert light_setup.call_count == 1
assert len(switch_setup.mock_calls) == 1 assert switch_setup.call_count == 1
async def test_no_config_creates_no_entry(hass): async def test_no_config_creates_no_entry(hass):
"""Test for when there is no tplink in config.""" """Test for when there is no tplink in config."""
with MOCK_PYHS100, patch( with MOCK_PYHS100, patch(
"homeassistant.components.tplink.async_setup_entry", "homeassistant.components.tplink.async_setup_entry",
return_value=mock_coro(True), return_value=mock_coro(True),
) as mock_setup: ) as mock_setup:
await async_setup_component(hass, tplink.DOMAIN, {}) await async_setup_component(hass, tplink.DOMAIN, {})
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 0 assert mock_setup.call_count == 0
@pytest.mark.parametrize("platform", ["switch", "light"]) @pytest.mark.parametrize("platform", ["switch", "light"])
@ -161,14 +235,14 @@ async def test_unload(hass, platform):
entry.add_to_hass(hass) entry.add_to_hass(hass)
with patch("pyHS100.SmartDevice._query_helper"), patch( with patch("pyHS100.SmartDevice._query_helper"), patch(
"homeassistant.components.tplink.{}" "homeassistant.components.tplink.{}"
".async_setup_entry".format(platform), ".async_setup_entry".format(platform),
return_value=mock_coro(True), return_value=mock_coro(True),
) as light_setup: ) as light_setup:
config = { config = {
"tplink": { tplink.DOMAIN: {
platform: [{"host": "123.123.123.123"}], platform: [{CONF_HOST: "123.123.123.123"}],
"discovery": False, CONF_DISCOVERY: False,
} }
} }
assert await async_setup_component(hass, tplink.DOMAIN, config) assert await async_setup_component(hass, tplink.DOMAIN, config)