Convert Hue to always use config entries (#13034)

This commit is contained in:
Paulus Schoutsen 2018-03-29 20:15:40 -07:00 committed by GitHub
parent 1ae8b6ee08
commit 184f2be83e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 914 additions and 527 deletions

View File

@ -13,6 +13,7 @@ import os
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.const import EVENT_HOMEASSISTANT_START
import homeassistant.helpers.config_validation as cv
@ -40,6 +41,10 @@ SERVICE_DECONZ = 'deconz'
SERVICE_DAIKIN = 'daikin'
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
CONFIG_ENTRY_HANDLERS = {
SERVICE_HUE: 'hue',
}
SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None),
SERVICE_NETGEAR: ('device_tracker', None),
@ -51,7 +56,6 @@ SERVICE_HANDLERS = {
SERVICE_WINK: ('wink', None),
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None),
SERVICE_HUE: ('hue', None),
SERVICE_DECONZ: ('deconz', None),
SERVICE_DAIKIN: ('daikin', None),
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
@ -105,6 +109,20 @@ async def async_setup(hass, config):
logger.info("Ignoring service: %s %s", service, info)
return
discovery_hash = json.dumps([service, info], sort_keys=True)
if discovery_hash in already_discovered:
return
already_discovered.add(discovery_hash)
if service in CONFIG_ENTRY_HANDLERS:
await hass.config_entries.flow.async_init(
CONFIG_ENTRY_HANDLERS[service],
source=config_entries.SOURCE_DISCOVERY,
data=info
)
return
comp_plat = SERVICE_HANDLERS.get(service)
# We do not know how to handle this service.
@ -112,12 +130,6 @@ async def async_setup(hass, config):
logger.info("Unknown service discovered: %s %s", service, info)
return
discovery_hash = json.dumps([service, info], sort_keys=True)
if discovery_hash in already_discovered:
return
already_discovered.add(discovery_hash)
logger.info("Found new service: %s %s", service, info)
component, platform = comp_plat

View File

@ -4,31 +4,23 @@ This component provides basic support for the Philips Hue system.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/hue/
"""
import asyncio
import json
import ipaddress
import logging
import os
import async_timeout
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.discovery import SERVICE_HUE
from homeassistant.const import CONF_FILENAME, CONF_HOST
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery, aiohttp_client
from homeassistant import config_entries
from homeassistant.util.json import save_json
from homeassistant.helpers import aiohttp_client, config_validation as cv
from .const import DOMAIN, API_NUPNP
from .bridge import HueBridge
# Loading the config flow file will register the flow
from .config_flow import configured_hosts
REQUIREMENTS = ['aiohue==1.3.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = "hue"
SERVICE_HUE_SCENE = "hue_activate_scene"
API_NUPNP = 'https://www.meethue.com/api/nupnp'
CONF_BRIDGES = "bridges"
CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
@ -42,6 +34,7 @@ DEFAULT_ALLOW_HUE_GROUPS = True
BRIDGE_CONFIG_SCHEMA = vol.Schema({
# Validate as IP address and then convert back to a string.
vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string),
# This is for legacy reasons and is only used for importing auth.
vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string,
vol.Optional(CONF_ALLOW_UNREACHABLE,
default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean,
@ -56,19 +49,6 @@ CONFIG_SCHEMA = vol.Schema({
}),
}, extra=vol.ALLOW_EXTRA)
ATTR_GROUP_NAME = "group_name"
ATTR_SCENE_NAME = "scene_name"
SCENE_SCHEMA = vol.Schema({
vol.Required(ATTR_GROUP_NAME): cv.string,
vol.Required(ATTR_SCENE_NAME): cv.string,
})
CONFIG_INSTRUCTIONS = """
Press the button on the bridge to register Philips Hue with Home Assistant.
![Location of button on bridge](/static/images/config_philips_hue.jpg)
"""
async def async_setup(hass, config):
"""Set up the Hue platform."""
@ -76,20 +56,8 @@ async def async_setup(hass, config):
if conf is None:
conf = {}
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
async def async_bridge_discovered(service, discovery_info):
"""Dispatcher for Hue discovery events."""
# Ignore emulated hue
if "HASS Bridge" in discovery_info.get('name', ''):
return
await async_setup_bridge(
hass, discovery_info['host'],
'phue-{}.conf'.format(discovery_info['serial']))
discovery.async_listen(hass, SERVICE_HUE, async_bridge_discovered)
hass.data[DOMAIN] = {}
configured = configured_hosts(hass)
# User has configured bridges
if CONF_BRIDGES in conf:
@ -103,12 +71,19 @@ async def async_setup(hass, config):
async with websession.get(API_NUPNP) as req:
hosts = await req.json()
# Run through config schema to populate defaults
bridges = [BRIDGE_CONFIG_SCHEMA({
CONF_HOST: entry['internalipaddress'],
CONF_FILENAME: '.hue_{}.conf'.format(entry['id']),
}) for entry in hosts]
bridges = []
for entry in hosts:
# Filter out already configured hosts
if entry['internalipaddress'] in configured:
continue
# Run through config schema to populate defaults
bridges.append(BRIDGE_CONFIG_SCHEMA({
CONF_HOST: entry['internalipaddress'],
# Careful with using entry['id'] for other reasons. The
# value is in lowercase but is returned uppercase from hub.
CONF_FILENAME: '.hue_{}.conf'.format(entry['id']),
}))
else:
# Component not specified in config, we're loaded via discovery
bridges = []
@ -116,277 +91,43 @@ async def async_setup(hass, config):
if not bridges:
return True
await asyncio.wait([
async_setup_bridge(
hass, bridge[CONF_HOST], bridge[CONF_FILENAME],
bridge[CONF_ALLOW_UNREACHABLE], bridge[CONF_ALLOW_HUE_GROUPS]
) for bridge in bridges
])
for bridge_conf in bridges:
host = bridge_conf[CONF_HOST]
# Store config in hass.data so the config entry can find it
hass.data[DOMAIN][host] = bridge_conf
# If configured, the bridge will be set up during config entry phase
if host in configured:
continue
# No existing config entry found, try importing it or trigger link
# config flow if no existing auth. Because we're inside the setup of
# this component we'll have to use hass.async_add_job to avoid a
# deadlock: creating a config entry will set up the component but the
# setup would block till the entry is created!
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, source='import', data={
'host': bridge_conf[CONF_HOST],
'path': bridge_conf[CONF_FILENAME],
}
))
return True
async def async_setup_bridge(
hass, host, filename=None,
allow_unreachable=DEFAULT_ALLOW_UNREACHABLE,
allow_hue_groups=DEFAULT_ALLOW_HUE_GROUPS,
username=None):
"""Set up a given Hue bridge."""
assert filename or username, 'Need to pass at least a username or filename'
# Only register a device once
if host in hass.data[DOMAIN]:
return
if username is None:
username = await hass.async_add_job(
_find_username_from_config, hass, filename)
bridge = HueBridge(host, hass, filename, username, allow_unreachable,
allow_hue_groups)
await bridge.async_setup()
def _find_username_from_config(hass, filename):
"""Load username from config."""
path = hass.config.path(filename)
if not os.path.isfile(path):
return None
with open(path) as inp:
return list(json.load(inp).values())[0]['username']
class HueBridge(object):
"""Manages a single Hue bridge."""
def __init__(self, host, hass, filename, username,
allow_unreachable=False, allow_groups=True):
"""Initialize the system."""
self.host = host
self.hass = hass
self.filename = filename
self.username = username
self.allow_unreachable = allow_unreachable
self.allow_groups = allow_groups
self.available = True
self.config_request_id = None
self.api = None
async def async_setup(self):
"""Set up a phue bridge based on host parameter."""
import aiohue
api = aiohue.Bridge(
self.host,
username=self.username,
websession=aiohttp_client.async_get_clientsession(self.hass)
)
try:
with async_timeout.timeout(5):
# Initialize bridge and validate our username
if not self.username:
await api.create_user('home-assistant')
await api.initialize()
except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized):
_LOGGER.warning("Connected to Hue at %s but not registered.",
self.host)
self.async_request_configuration()
return
except (asyncio.TimeoutError, aiohue.RequestError):
_LOGGER.error("Error connecting to the Hue bridge at %s",
self.host)
return
except aiohue.AiohueException:
_LOGGER.exception('Unknown Hue linking error occurred')
self.async_request_configuration()
return
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unknown error connecting with Hue bridge at %s",
self.host)
return
self.hass.data[DOMAIN][self.host] = self
# If we came here and configuring this host, mark as done
if self.config_request_id:
request_id = self.config_request_id
self.config_request_id = None
self.hass.components.configurator.async_request_done(request_id)
self.username = api.username
# Save config file
await self.hass.async_add_job(
save_json, self.hass.config.path(self.filename),
{self.host: {'username': api.username}})
self.api = api
self.hass.async_add_job(discovery.async_load_platform(
self.hass, 'light', DOMAIN,
{'host': self.host}))
self.hass.services.async_register(
DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene,
schema=SCENE_SCHEMA)
@callback
def async_request_configuration(self):
"""Request configuration steps from the user."""
configurator = self.hass.components.configurator
# We got an error if this method is called while we are configuring
if self.config_request_id:
configurator.async_notify_errors(
self.config_request_id,
"Failed to register, please try again.")
return
async def config_callback(data):
"""Callback for configurator data."""
await self.async_setup()
self.config_request_id = configurator.async_request_config(
"Philips Hue", config_callback,
description=CONFIG_INSTRUCTIONS,
entity_picture="/static/images/logo_philips_hue.png",
submit_caption="I have pressed the button"
)
async def hue_activate_scene(self, call, updated=False):
"""Service to call directly into bridge to set scenes."""
group_name = call.data[ATTR_GROUP_NAME]
scene_name = call.data[ATTR_SCENE_NAME]
group = next(
(group for group in self.api.groups.values()
if group.name == group_name), None)
scene_id = next(
(scene.id for scene in self.api.scenes.values()
if scene.name == scene_name), None)
# If we can't find it, fetch latest info.
if not updated and (group is None or scene_id is None):
await self.api.groups.update()
await self.api.scenes.update()
await self.hue_activate_scene(call, updated=True)
return
if group is None:
_LOGGER.warning('Unable to find group %s', group_name)
return
if scene_id is None:
_LOGGER.warning('Unable to find scene %s', scene_name)
return
await group.set_action(scene=scene_id)
@config_entries.HANDLERS.register(DOMAIN)
class HueFlowHandler(config_entries.ConfigFlowHandler):
"""Handle a Hue config flow."""
VERSION = 1
def __init__(self):
"""Initialize the Hue flow."""
self.host = None
@property
def _websession(self):
"""Return a websession.
Cannot assign in init because hass variable is not set yet.
"""
return aiohttp_client.async_get_clientsession(self.hass)
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
from aiohue.discovery import discover_nupnp
if user_input is not None:
self.host = user_input['host']
return await self.async_step_link()
try:
with async_timeout.timeout(5):
bridges = await discover_nupnp(websession=self._websession)
except asyncio.TimeoutError:
return self.async_abort(
reason='discover_timeout'
)
if not bridges:
return self.async_abort(
reason='no_bridges'
)
# Find already configured hosts
configured_hosts = set(
entry.data['host'] for entry
in self.hass.config_entries.async_entries(DOMAIN))
hosts = [bridge.host for bridge in bridges
if bridge.host not in configured_hosts]
if not hosts:
return self.async_abort(
reason='all_configured'
)
elif len(hosts) == 1:
self.host = hosts[0]
return await self.async_step_link()
return self.async_show_form(
step_id='init',
data_schema=vol.Schema({
vol.Required('host'): vol.In(hosts)
})
)
async def async_step_link(self, user_input=None):
"""Attempt to link with the Hue bridge."""
import aiohue
errors = {}
if user_input is not None:
bridge = aiohue.Bridge(self.host, websession=self._websession)
try:
with async_timeout.timeout(5):
# Create auth token
await bridge.create_user('home-assistant')
# Fetches name and id
await bridge.initialize()
except (asyncio.TimeoutError, aiohue.RequestError,
aiohue.LinkButtonNotPressed):
errors['base'] = 'register_failed'
except aiohue.AiohueException:
errors['base'] = 'linking'
_LOGGER.exception('Unknown Hue linking error occurred')
else:
return self.async_create_entry(
title=bridge.config.name,
data={
'host': bridge.host,
'bridge_id': bridge.config.bridgeid,
'username': bridge.username,
}
)
return self.async_show_form(
step_id='link',
errors=errors,
)
async def async_setup_entry(hass, entry):
"""Set up a bridge for a config entry."""
await async_setup_bridge(hass, entry.data['host'],
username=entry.data['username'])
return True
"""Set up a bridge from a config entry."""
host = entry.data['host']
config = hass.data[DOMAIN].get(host)
if config is None:
allow_unreachable = DEFAULT_ALLOW_UNREACHABLE
allow_groups = DEFAULT_ALLOW_HUE_GROUPS
else:
allow_unreachable = config[CONF_ALLOW_UNREACHABLE]
allow_groups = config[CONF_ALLOW_HUE_GROUPS]
bridge = HueBridge(hass, entry, allow_unreachable, allow_groups)
hass.data[DOMAIN][host] = bridge
return await bridge.async_setup()

View File

@ -0,0 +1,143 @@
"""Code to handle a Hue bridge."""
import asyncio
import async_timeout
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.helpers import aiohttp_client, config_validation as cv
from .const import DOMAIN, LOGGER
from .errors import AuthenticationRequired, CannotConnect
SERVICE_HUE_SCENE = "hue_activate_scene"
ATTR_GROUP_NAME = "group_name"
ATTR_SCENE_NAME = "scene_name"
SCENE_SCHEMA = vol.Schema({
vol.Required(ATTR_GROUP_NAME): cv.string,
vol.Required(ATTR_SCENE_NAME): cv.string,
})
class HueBridge(object):
"""Manages a single Hue bridge."""
def __init__(self, hass, config_entry, allow_unreachable, allow_groups):
"""Initialize the system."""
self.config_entry = config_entry
self.hass = hass
self.allow_unreachable = allow_unreachable
self.allow_groups = allow_groups
self.available = True
self.api = None
async def async_setup(self, tries=0):
"""Set up a phue bridge based on host parameter."""
host = self.config_entry.data['host']
try:
self.api = await get_bridge(
self.hass, host,
self.config_entry.data['username']
)
except AuthenticationRequired:
# usernames can become invalid if hub is reset or user removed.
# We are going to fail the config entry setup and initiate a new
# linking procedure. When linking succeeds, it will remove the
# old config entry.
self.hass.async_add_job(self.hass.config_entries.flow.async_init(
DOMAIN, source='import', data={
'host': host,
}
))
return False
except CannotConnect:
retry_delay = 2 ** (tries + 1)
LOGGER.error("Error connecting to the Hue bridge at %s. Retrying "
"in %d seconds", host, retry_delay)
async def retry_setup(_now):
"""Retry setup."""
if await self.async_setup(tries + 1):
# This feels hacky, we should find a better way to do this
self.config_entry.state = config_entries.ENTRY_STATE_LOADED
# Unhandled edge case: cancel this if we discover bridge on new IP
self.hass.helpers.event.async_call_later(retry_delay, retry_setup)
return False
except Exception: # pylint: disable=broad-except
LOGGER.exception('Unknown error connecting with Hue bridge at %s',
host)
return False
self.hass.async_add_job(
self.hass.helpers.discovery.async_load_platform(
'light', DOMAIN, {'host': host}))
self.hass.services.async_register(
DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene,
schema=SCENE_SCHEMA)
return True
async def hue_activate_scene(self, call, updated=False):
"""Service to call directly into bridge to set scenes."""
group_name = call.data[ATTR_GROUP_NAME]
scene_name = call.data[ATTR_SCENE_NAME]
group = next(
(group for group in self.api.groups.values()
if group.name == group_name), None)
scene_id = next(
(scene.id for scene in self.api.scenes.values()
if scene.name == scene_name), None)
# If we can't find it, fetch latest info.
if not updated and (group is None or scene_id is None):
await self.api.groups.update()
await self.api.scenes.update()
await self.hue_activate_scene(call, updated=True)
return
if group is None:
LOGGER.warning('Unable to find group %s', group_name)
return
if scene_id is None:
LOGGER.warning('Unable to find scene %s', scene_name)
return
await group.set_action(scene=scene_id)
async def get_bridge(hass, host, username=None):
"""Create a bridge object and verify authentication."""
import aiohue
bridge = aiohue.Bridge(
host, username=username,
websession=aiohttp_client.async_get_clientsession(hass)
)
try:
with async_timeout.timeout(5):
# Create username if we don't have one
if not username:
await bridge.create_user('home-assistant')
# Initialize bridge (and validate our username)
await bridge.initialize()
return bridge
except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized):
LOGGER.warning("Connected to Hue at %s but not registered.", host)
raise AuthenticationRequired
except (asyncio.TimeoutError, aiohue.RequestError):
LOGGER.error("Error connecting to the Hue bridge at %s", host)
raise CannotConnect
except aiohue.AiohueException:
LOGGER.exception('Unknown Hue linking error occurred')
raise AuthenticationRequired

View File

@ -0,0 +1,235 @@
"""Config flow to configure Philips Hue."""
import asyncio
import json
import os
import async_timeout
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
from .bridge import get_bridge
from .const import DOMAIN, LOGGER
from .errors import AuthenticationRequired, CannotConnect
@callback
def configured_hosts(hass):
"""Return a set of the configured hosts."""
return set(entry.data['host'] for entry
in hass.config_entries.async_entries(DOMAIN))
def _find_username_from_config(hass, filename):
"""Load username from config.
This was a legacy way of configuring Hue until Home Assistant 0.67.
"""
path = hass.config.path(filename)
if not os.path.isfile(path):
return None
with open(path) as inp:
try:
return list(json.load(inp).values())[0]['username']
except ValueError:
# If we get invalid JSON
return None
@config_entries.HANDLERS.register(DOMAIN)
class HueFlowHandler(config_entries.ConfigFlowHandler):
"""Handle a Hue config flow."""
VERSION = 1
def __init__(self):
"""Initialize the Hue flow."""
self.host = None
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
from aiohue.discovery import discover_nupnp
if user_input is not None:
self.host = user_input['host']
return await self.async_step_link()
websession = aiohttp_client.async_get_clientsession(self.hass)
try:
with async_timeout.timeout(5):
bridges = await discover_nupnp(websession=websession)
except asyncio.TimeoutError:
return self.async_abort(
reason='discover_timeout'
)
if not bridges:
return self.async_abort(
reason='no_bridges'
)
# Find already configured hosts
configured = configured_hosts(self.hass)
hosts = [bridge.host for bridge in bridges
if bridge.host not in configured]
if not hosts:
return self.async_abort(
reason='all_configured'
)
elif len(hosts) == 1:
self.host = hosts[0]
return await self.async_step_link()
return self.async_show_form(
step_id='init',
data_schema=vol.Schema({
vol.Required('host'): vol.In(hosts)
})
)
async def async_step_link(self, user_input=None):
"""Attempt to link with the Hue bridge.
Given a configured host, will ask the user to press the link button
to connect to the bridge.
"""
errors = {}
# We will always try linking in case the user has already pressed
# the link button.
try:
bridge = await get_bridge(
self.hass, self.host, username=None
)
return await self._entry_from_bridge(bridge)
except AuthenticationRequired:
errors['base'] = 'register_failed'
except CannotConnect:
LOGGER.error("Error connecting to the Hue bridge at %s", self.host)
errors['base'] = 'linking'
except Exception: # pylint: disable=broad-except
LOGGER.exception(
'Unknown error connecting with Hue bridge at %s',
self.host)
errors['base'] = 'linking'
# If there was no user input, do not show the errors.
if user_input is None:
errors = {}
return self.async_show_form(
step_id='link',
errors=errors,
)
async def async_step_discovery(self, discovery_info):
"""Handle a discovered Hue bridge.
This flow is triggered by the discovery component. It will check if the
host is already configured and delegate to the import step if not.
"""
# Filter out emulated Hue
if "HASS Bridge" in discovery_info.get('name', ''):
return self.async_abort(reason='already_configured')
host = discovery_info.get('host')
if host in configured_hosts(self.hass):
return self.async_abort(reason='already_configured')
# This value is based off host/description.xml and is, weirdly, missing
# 4 characters in the middle of the serial compared to results returned
# from the NUPNP API or when querying the bridge API for bridgeid.
# (on first gen Hue hub)
serial = discovery_info.get('serial')
return await self.async_step_import({
'host': host,
# This format is the legacy format that Hue used for discovery
'path': 'phue-{}.conf'.format(serial)
})
async def async_step_import(self, import_info):
"""Import a new bridge as a config entry.
Will read authentication from Phue config file if available.
This flow is triggered by `async_setup` for both configured and
discovered bridges. Triggered for any bridge that does not have a
config entry yet (based on host).
This flow is also triggered by `async_step_discovery`.
If an existing config file is found, we will validate the credentials
and create an entry. Otherwise we will delegate to `link` step which
will ask user to link the bridge.
"""
host = import_info['host']
path = import_info.get('path')
if path is not None:
username = await self.hass.async_add_job(
_find_username_from_config, self.hass,
self.hass.config.path(path))
else:
username = None
try:
bridge = await get_bridge(
self.hass, host, username
)
LOGGER.info('Imported authentication for %s from %s', host, path)
return await self._entry_from_bridge(bridge)
except AuthenticationRequired:
self.host = host
LOGGER.info('Invalid authentication for %s, requesting link.',
host)
return await self.async_step_link()
except CannotConnect:
LOGGER.error("Error connecting to the Hue bridge at %s", host)
return self.async_abort(reason='cannot_connect')
except Exception: # pylint: disable=broad-except
LOGGER.exception('Unknown error connecting with Hue bridge at %s',
host)
return self.async_abort(reason='unknown')
async def _entry_from_bridge(self, bridge):
"""Return a config entry from an initialized bridge."""
# Remove all other entries of hubs with same ID or host
host = bridge.host
bridge_id = bridge.config.bridgeid
same_hub_entries = [entry.entry_id for entry
in self.hass.config_entries.async_entries(DOMAIN)
if entry.data['bridge_id'] == bridge_id or
entry.data['host'] == host]
if same_hub_entries:
await asyncio.wait([self.hass.config_entries.async_remove(entry_id)
for entry_id in same_hub_entries])
return self.async_create_entry(
title=bridge.config.name,
data={
'host': host,
'bridge_id': bridge_id,
'username': bridge.username,
}
)

View File

@ -0,0 +1,6 @@
"""Constants for the Hue component."""
import logging
LOGGER = logging.getLogger('homeassistant.components.hue')
DOMAIN = "hue"
API_NUPNP = 'https://www.meethue.com/api/nupnp'

View File

@ -0,0 +1,14 @@
"""Errors for the Hue component."""
from homeassistant.exceptions import HomeAssistantError
class HueException(HomeAssistantError):
"""Base class for Hue exceptions."""
class CannotConnect(HueException):
"""Unable to connect to the bridge."""
class AuthenticationRequired(HueException):
"""Unknown error occurred."""

View File

@ -20,7 +20,10 @@
"abort": {
"discover_timeout": "Unable to discover Hue bridges",
"no_bridges": "No Philips Hue bridges discovered",
"all_configured": "All Philips Hue bridges are already configured"
"all_configured": "All Philips Hue bridges are already configured",
"unknown": "Unknown error occurred",
"cannot_connect": "Unable to connect to the bridge",
"already_configured": "Bridge is already configured"
}
}
}

View File

@ -384,7 +384,7 @@ class FlowManager:
handler = HANDLERS.get(domain)
if handler is None:
raise self.hass.helpers.UnknownHandler
raise UnknownHandler
# Make sure requirements and dependencies of component are resolved
await async_process_deps_reqs(

View File

@ -1,17 +0,0 @@
"""Fixtures for Hue tests."""
from unittest.mock import patch
import pytest
from tests.common import mock_coro_func
@pytest.fixture
def mock_bridge():
"""Mock the HueBridge from initializing."""
with patch('homeassistant.components.hue._find_username_from_config',
return_value=None), \
patch('homeassistant.components.hue.HueBridge') as mock_bridge:
mock_bridge().async_setup = mock_coro_func()
mock_bridge.reset_mock()
yield mock_bridge

View File

@ -1,99 +1,57 @@
"""Test Hue bridge."""
import asyncio
from unittest.mock import Mock, patch
import aiohue
import pytest
from homeassistant.components import hue
from homeassistant.components.hue import bridge, errors
from tests.common import mock_coro
class MockBridge(hue.HueBridge):
"""Class that sets default for constructor."""
async def test_bridge_setup():
"""Test a successful setup."""
hass = Mock()
entry = Mock()
api = Mock()
entry.data = {'host': '1.2.3.4', 'username': 'mock-username'}
hue_bridge = bridge.HueBridge(hass, entry, False, False)
def __init__(self, hass, host='1.2.3.4', filename='mock-bridge.conf',
username=None, **kwargs):
"""Initialize a mock bridge."""
super().__init__(host, hass, filename, username, **kwargs)
with patch.object(bridge, 'get_bridge', return_value=mock_coro(api)):
assert await hue_bridge.async_setup() is True
@pytest.fixture
def mock_request():
"""Mock configurator.async_request_config."""
with patch('homeassistant.components.configurator.'
'async_request_config') as mock_request:
yield mock_request
async def test_setup_request_config_button_not_pressed(hass, mock_request):
"""Test we request config if link button has not been pressed."""
with patch('aiohue.Bridge.create_user',
side_effect=aiohue.LinkButtonNotPressed):
await MockBridge(hass).async_setup()
assert len(mock_request.mock_calls) == 1
async def test_setup_request_config_invalid_username(hass, mock_request):
"""Test we request config if username is no longer whitelisted."""
with patch('aiohue.Bridge.create_user',
side_effect=aiohue.Unauthorized):
await MockBridge(hass).async_setup()
assert len(mock_request.mock_calls) == 1
async def test_setup_timeout(hass, mock_request):
"""Test we give up when there is a timeout."""
with patch('aiohue.Bridge.create_user',
side_effect=asyncio.TimeoutError):
await MockBridge(hass).async_setup()
assert len(mock_request.mock_calls) == 0
async def test_only_create_no_username(hass):
"""."""
with patch('aiohue.Bridge.create_user') as mock_create, \
patch('aiohue.Bridge.initialize') as mock_init:
await MockBridge(hass, username='bla').async_setup()
assert len(mock_create.mock_calls) == 0
assert len(mock_init.mock_calls) == 1
async def test_configurator_callback(hass, mock_request):
"""."""
hass.data[hue.DOMAIN] = {}
with patch('aiohue.Bridge.create_user',
side_effect=aiohue.LinkButtonNotPressed):
await MockBridge(hass).async_setup()
assert len(mock_request.mock_calls) == 1
callback = mock_request.mock_calls[0][1][2]
mock_init = Mock(return_value=mock_coro())
mock_create = Mock(return_value=mock_coro())
with patch('aiohue.Bridge') as mock_bridge, \
patch('homeassistant.helpers.discovery.async_load_platform',
return_value=mock_coro()) as mock_load_platform, \
patch('homeassistant.components.hue.save_json') as mock_save:
inst = mock_bridge()
inst.username = 'mock-user'
inst.create_user = mock_create
inst.initialize = mock_init
await callback(None)
assert len(mock_create.mock_calls) == 1
assert len(mock_init.mock_calls) == 1
assert len(mock_save.mock_calls) == 1
assert mock_save.mock_calls[0][1][1] == {
'1.2.3.4': {
'username': 'mock-user'
}
assert hue_bridge.api is api
assert len(hass.helpers.discovery.async_load_platform.mock_calls) == 1
assert hass.helpers.discovery.async_load_platform.mock_calls[0][1][2] == {
'host': '1.2.3.4'
}
assert len(mock_load_platform.mock_calls) == 1
async def test_bridge_setup_invalid_username():
"""Test we start config flow if username is no longer whitelisted."""
hass = Mock()
entry = Mock()
entry.data = {'host': '1.2.3.4', 'username': 'mock-username'}
hue_bridge = bridge.HueBridge(hass, entry, False, False)
with patch.object(bridge, 'get_bridge',
side_effect=errors.AuthenticationRequired):
assert await hue_bridge.async_setup() is False
assert len(hass.async_add_job.mock_calls) == 1
assert len(hass.config_entries.flow.async_init.mock_calls) == 1
assert hass.config_entries.flow.async_init.mock_calls[0][2]['data'] == {
'host': '1.2.3.4'
}
async def test_bridge_setup_timeout(hass):
"""Test we retry to connect if we cannot connect."""
hass = Mock()
entry = Mock()
entry.data = {'host': '1.2.3.4', 'username': 'mock-username'}
hue_bridge = bridge.HueBridge(hass, entry, False, False)
with patch.object(bridge, 'get_bridge', side_effect=errors.CannotConnect):
assert await hue_bridge.async_setup() is False
assert len(hass.helpers.event.async_call_later.mock_calls) == 1
# Assert we are going to wait 2 seconds
assert hass.helpers.event.async_call_later.mock_calls[0][1][0] == 2

View File

@ -1,28 +1,29 @@
"""Tests for Philips Hue config flow."""
import asyncio
from unittest.mock import patch
from unittest.mock import Mock, patch
import aiohue
import pytest
import voluptuous as vol
from homeassistant.components import hue
from homeassistant.components.hue import config_flow, const, errors
from tests.common import MockConfigEntry, mock_coro
async def test_flow_works(hass, aioclient_mock):
"""Test config flow ."""
aioclient_mock.get(hue.API_NUPNP, json=[
aioclient_mock.get(const.API_NUPNP, json=[
{'internalipaddress': '1.2.3.4', 'id': 'bla'}
])
flow = hue.HueFlowHandler()
flow = config_flow.HueFlowHandler()
flow.hass = hass
await flow.async_step_init()
with patch('aiohue.Bridge') as mock_bridge:
def mock_constructor(host, websession):
def mock_constructor(host, websession, username=None):
"""Fake the bridge constructor."""
mock_bridge.host = host
return mock_bridge
@ -50,8 +51,8 @@ async def test_flow_works(hass, aioclient_mock):
async def test_flow_no_discovered_bridges(hass, aioclient_mock):
"""Test config flow discovers no bridges."""
aioclient_mock.get(hue.API_NUPNP, json=[])
flow = hue.HueFlowHandler()
aioclient_mock.get(const.API_NUPNP, json=[])
flow = config_flow.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_init()
@ -60,13 +61,13 @@ async def test_flow_no_discovered_bridges(hass, aioclient_mock):
async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock):
"""Test config flow discovers only already configured bridges."""
aioclient_mock.get(hue.API_NUPNP, json=[
aioclient_mock.get(const.API_NUPNP, json=[
{'internalipaddress': '1.2.3.4', 'id': 'bla'}
])
MockConfigEntry(domain='hue', data={
'host': '1.2.3.4'
}).add_to_hass(hass)
flow = hue.HueFlowHandler()
flow = config_flow.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_init()
@ -75,10 +76,10 @@ async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock):
async def test_flow_one_bridge_discovered(hass, aioclient_mock):
"""Test config flow discovers one bridge."""
aioclient_mock.get(hue.API_NUPNP, json=[
aioclient_mock.get(const.API_NUPNP, json=[
{'internalipaddress': '1.2.3.4', 'id': 'bla'}
])
flow = hue.HueFlowHandler()
flow = config_flow.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_init()
@ -88,11 +89,11 @@ async def test_flow_one_bridge_discovered(hass, aioclient_mock):
async def test_flow_two_bridges_discovered(hass, aioclient_mock):
"""Test config flow discovers two bridges."""
aioclient_mock.get(hue.API_NUPNP, json=[
aioclient_mock.get(const.API_NUPNP, json=[
{'internalipaddress': '1.2.3.4', 'id': 'bla'},
{'internalipaddress': '5.6.7.8', 'id': 'beer'}
])
flow = hue.HueFlowHandler()
flow = config_flow.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_init()
@ -108,14 +109,14 @@ async def test_flow_two_bridges_discovered(hass, aioclient_mock):
async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock):
"""Test config flow discovers two bridges."""
aioclient_mock.get(hue.API_NUPNP, json=[
aioclient_mock.get(const.API_NUPNP, json=[
{'internalipaddress': '1.2.3.4', 'id': 'bla'},
{'internalipaddress': '5.6.7.8', 'id': 'beer'}
])
MockConfigEntry(domain='hue', data={
'host': '1.2.3.4'
}).add_to_hass(hass)
flow = hue.HueFlowHandler()
flow = config_flow.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_init()
@ -126,7 +127,7 @@ async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock):
async def test_flow_timeout_discovery(hass):
"""Test config flow ."""
flow = hue.HueFlowHandler()
flow = config_flow.HueFlowHandler()
flow.hass = hass
with patch('aiohue.discovery.discover_nupnp',
@ -138,7 +139,7 @@ async def test_flow_timeout_discovery(hass):
async def test_flow_link_timeout(hass):
"""Test config flow ."""
flow = hue.HueFlowHandler()
flow = config_flow.HueFlowHandler()
flow.hass = hass
with patch('aiohue.Bridge.create_user',
@ -148,13 +149,13 @@ async def test_flow_link_timeout(hass):
assert result['type'] == 'form'
assert result['step_id'] == 'link'
assert result['errors'] == {
'base': 'register_failed'
'base': 'linking'
}
async def test_flow_link_button_not_pressed(hass):
"""Test config flow ."""
flow = hue.HueFlowHandler()
flow = config_flow.HueFlowHandler()
flow.hass = hass
with patch('aiohue.Bridge.create_user',
@ -170,7 +171,7 @@ async def test_flow_link_button_not_pressed(hass):
async def test_flow_link_unknown_host(hass):
"""Test config flow ."""
flow = hue.HueFlowHandler()
flow = config_flow.HueFlowHandler()
flow.hass = hass
with patch('aiohue.Bridge.create_user',
@ -180,5 +181,175 @@ async def test_flow_link_unknown_host(hass):
assert result['type'] == 'form'
assert result['step_id'] == 'link'
assert result['errors'] == {
'base': 'register_failed'
'base': 'linking'
}
async def test_bridge_discovery(hass):
"""Test a bridge being discovered."""
flow = config_flow.HueFlowHandler()
flow.hass = hass
with patch.object(config_flow, 'get_bridge',
side_effect=errors.AuthenticationRequired):
result = await flow.async_step_discovery({
'host': '0.0.0.0',
'serial': '1234'
})
assert result['type'] == 'form'
assert result['step_id'] == 'link'
async def test_bridge_discovery_emulated_hue(hass):
"""Test if discovery info is from an emulated hue instance."""
flow = config_flow.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_discovery({
'name': 'HASS Bridge',
'host': '0.0.0.0',
'serial': '1234'
})
assert result['type'] == 'abort'
async def test_bridge_discovery_already_configured(hass):
"""Test if a discovered bridge has already been configured."""
MockConfigEntry(domain='hue', data={
'host': '0.0.0.0'
}).add_to_hass(hass)
flow = config_flow.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_discovery({
'host': '0.0.0.0',
'serial': '1234'
})
assert result['type'] == 'abort'
async def test_import_with_existing_config(hass):
"""Test importing a host with an existing config file."""
flow = config_flow.HueFlowHandler()
flow.hass = hass
bridge = Mock()
bridge.username = 'username-abc'
bridge.config.bridgeid = 'bridge-id-1234'
bridge.config.name = 'Mock Bridge'
bridge.host = '0.0.0.0'
with patch.object(config_flow, '_find_username_from_config',
return_value='mock-user'), \
patch.object(config_flow, 'get_bridge',
return_value=mock_coro(bridge)):
result = await flow.async_step_import({
'host': '0.0.0.0',
'path': 'bla.conf'
})
assert result['type'] == 'create_entry'
assert result['title'] == 'Mock Bridge'
assert result['data'] == {
'host': '0.0.0.0',
'bridge_id': 'bridge-id-1234',
'username': 'username-abc'
}
async def test_import_with_no_config(hass):
"""Test importing a host without an existing config file."""
flow = config_flow.HueFlowHandler()
flow.hass = hass
with patch.object(config_flow, 'get_bridge',
side_effect=errors.AuthenticationRequired):
result = await flow.async_step_import({
'host': '0.0.0.0',
})
assert result['type'] == 'form'
assert result['step_id'] == 'link'
async def test_import_with_existing_but_invalid_config(hass):
"""Test importing a host with a config file with invalid username."""
flow = config_flow.HueFlowHandler()
flow.hass = hass
with patch.object(config_flow, '_find_username_from_config',
return_value='mock-user'), \
patch.object(config_flow, 'get_bridge',
side_effect=errors.AuthenticationRequired):
result = await flow.async_step_import({
'host': '0.0.0.0',
'path': 'bla.conf'
})
assert result['type'] == 'form'
assert result['step_id'] == 'link'
async def test_import_cannot_connect(hass):
"""Test importing a host that we cannot conncet to."""
flow = config_flow.HueFlowHandler()
flow.hass = hass
with patch.object(config_flow, 'get_bridge',
side_effect=errors.CannotConnect):
result = await flow.async_step_import({
'host': '0.0.0.0',
})
assert result['type'] == 'abort'
assert result['reason'] == 'cannot_connect'
async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass):
"""Test that we clean up entries for same host and bridge.
An IP can only hold a single bridge and a single bridge can only be
accessible via a single IP. So when we create a new entry, we'll remove
all existing entries that either have same IP or same bridge_id.
"""
MockConfigEntry(domain='hue', data={
'host': '0.0.0.0',
'bridge_id': 'id-1234'
}).add_to_hass(hass)
MockConfigEntry(domain='hue', data={
'host': '1.2.3.4',
'bridge_id': 'id-1234'
}).add_to_hass(hass)
assert len(hass.config_entries.async_entries('hue')) == 2
flow = config_flow.HueFlowHandler()
flow.hass = hass
bridge = Mock()
bridge.username = 'username-abc'
bridge.config.bridgeid = 'id-1234'
bridge.config.name = 'Mock Bridge'
bridge.host = '0.0.0.0'
with patch.object(config_flow, 'get_bridge',
return_value=mock_coro(bridge)):
result = await flow.async_step_import({
'host': '0.0.0.0',
})
assert result['type'] == 'create_entry'
assert result['title'] == 'Mock Bridge'
assert result['data'] == {
'host': '0.0.0.0',
'bridge_id': 'id-1234',
'username': 'username-abc'
}
# We did not process the result of this entry but already removed the old
# ones. So we should have 0 entries.
assert len(hass.config_entries.async_entries('hue')) == 0

View File

@ -0,0 +1,169 @@
"""Test Hue setup process."""
from unittest.mock import patch
from homeassistant.setup import async_setup_component
from homeassistant.components import hue
from tests.common import mock_coro, MockConfigEntry
async def test_setup_with_no_config(hass):
"""Test that we do not discover anything or try to setup a bridge."""
with patch.object(hass, 'config_entries') as mock_config_entries, \
patch.object(hue, 'configured_hosts', return_value=[]):
assert await async_setup_component(hass, hue.DOMAIN, {}) is True
# No flows started
assert len(mock_config_entries.flow.mock_calls) == 0
# No configs stored
assert hass.data[hue.DOMAIN] == {}
async def test_setup_with_discovery_no_known_auth(hass, aioclient_mock):
"""Test discovering a bridge and not having known auth."""
aioclient_mock.get(hue.API_NUPNP, json=[
{
'internalipaddress': '0.0.0.0',
'id': 'abcd1234'
}
])
with patch.object(hass, 'config_entries') as mock_config_entries, \
patch.object(hue, 'configured_hosts', return_value=[]):
mock_config_entries.flow.async_init.return_value = mock_coro()
assert await async_setup_component(hass, hue.DOMAIN, {
hue.DOMAIN: {}
}) is True
# Flow started for discovered bridge
assert len(mock_config_entries.flow.mock_calls) == 1
assert mock_config_entries.flow.mock_calls[0][2]['data'] == {
'host': '0.0.0.0',
'path': '.hue_abcd1234.conf',
}
# Config stored for domain.
assert hass.data[hue.DOMAIN] == {
'0.0.0.0': {
hue.CONF_HOST: '0.0.0.0',
hue.CONF_FILENAME: '.hue_abcd1234.conf',
hue.CONF_ALLOW_HUE_GROUPS: hue.DEFAULT_ALLOW_HUE_GROUPS,
hue.CONF_ALLOW_UNREACHABLE: hue.DEFAULT_ALLOW_UNREACHABLE,
}
}
async def test_setup_with_discovery_known_auth(hass, aioclient_mock):
"""Test we don't do anything if we discover already configured hub."""
aioclient_mock.get(hue.API_NUPNP, json=[
{
'internalipaddress': '0.0.0.0',
'id': 'abcd1234'
}
])
with patch.object(hass, 'config_entries') as mock_config_entries, \
patch.object(hue, 'configured_hosts', return_value=['0.0.0.0']):
assert await async_setup_component(hass, hue.DOMAIN, {
hue.DOMAIN: {}
}) is True
# Flow started for discovered bridge
assert len(mock_config_entries.flow.mock_calls) == 0
# Config stored for domain.
assert hass.data[hue.DOMAIN] == {}
async def test_setup_defined_hosts_known_auth(hass):
"""Test we don't initiate a config entry if config bridge is known."""
with patch.object(hass, 'config_entries') as mock_config_entries, \
patch.object(hue, 'configured_hosts', return_value=['0.0.0.0']):
assert await async_setup_component(hass, hue.DOMAIN, {
hue.DOMAIN: {
hue.CONF_BRIDGES: {
hue.CONF_HOST: '0.0.0.0',
hue.CONF_FILENAME: 'bla.conf',
hue.CONF_ALLOW_HUE_GROUPS: False,
hue.CONF_ALLOW_UNREACHABLE: True
}
}
}) is True
# Flow started for discovered bridge
assert len(mock_config_entries.flow.mock_calls) == 0
# Config stored for domain.
assert hass.data[hue.DOMAIN] == {
'0.0.0.0': {
hue.CONF_HOST: '0.0.0.0',
hue.CONF_FILENAME: 'bla.conf',
hue.CONF_ALLOW_HUE_GROUPS: False,
hue.CONF_ALLOW_UNREACHABLE: True
}
}
async def test_setup_defined_hosts_no_known_auth(hass):
"""Test we initiate config entry if config bridge is not known."""
with patch.object(hass, 'config_entries') as mock_config_entries, \
patch.object(hue, 'configured_hosts', return_value=[]):
mock_config_entries.flow.async_init.return_value = mock_coro()
assert await async_setup_component(hass, hue.DOMAIN, {
hue.DOMAIN: {
hue.CONF_BRIDGES: {
hue.CONF_HOST: '0.0.0.0',
hue.CONF_FILENAME: 'bla.conf',
hue.CONF_ALLOW_HUE_GROUPS: False,
hue.CONF_ALLOW_UNREACHABLE: True
}
}
}) is True
# Flow started for discovered bridge
assert len(mock_config_entries.flow.mock_calls) == 1
assert mock_config_entries.flow.mock_calls[0][2]['data'] == {
'host': '0.0.0.0',
'path': 'bla.conf',
}
# Config stored for domain.
assert hass.data[hue.DOMAIN] == {
'0.0.0.0': {
hue.CONF_HOST: '0.0.0.0',
hue.CONF_FILENAME: 'bla.conf',
hue.CONF_ALLOW_HUE_GROUPS: False,
hue.CONF_ALLOW_UNREACHABLE: True
}
}
async def test_config_passed_to_config_entry(hass):
"""Test that configured options for a host are loaded via config entry."""
entry = MockConfigEntry(domain=hue.DOMAIN, data={
'host': '0.0.0.0',
})
entry.add_to_hass(hass)
with patch.object(hue, 'HueBridge') as mock_bridge:
mock_bridge.return_value.async_setup.return_value = mock_coro(True)
assert await async_setup_component(hass, hue.DOMAIN, {
hue.DOMAIN: {
hue.CONF_BRIDGES: {
hue.CONF_HOST: '0.0.0.0',
hue.CONF_FILENAME: 'bla.conf',
hue.CONF_ALLOW_HUE_GROUPS: False,
hue.CONF_ALLOW_UNREACHABLE: True
}
}
}) is True
assert len(mock_bridge.mock_calls) == 2
p_hass, p_entry, p_allow_unreachable, p_allow_groups = \
mock_bridge.mock_calls[0][1]
assert p_hass is hass
assert p_entry is entry
assert p_allow_unreachable is True
assert p_allow_groups is False

View File

@ -1,70 +0,0 @@
"""Test Hue setup process."""
from homeassistant.setup import async_setup_component
from homeassistant.components import hue
from homeassistant.components.discovery import SERVICE_HUE
async def test_setup_with_multiple_hosts(hass, mock_bridge):
"""Multiple hosts specified in the config file."""
assert await async_setup_component(hass, hue.DOMAIN, {
hue.DOMAIN: {
hue.CONF_BRIDGES: [
{hue.CONF_HOST: '127.0.0.1'},
{hue.CONF_HOST: '192.168.1.10'},
]
}
})
assert len(mock_bridge.mock_calls) == 2
hosts = sorted(mock_call[1][0] for mock_call in mock_bridge.mock_calls)
assert hosts == ['127.0.0.1', '192.168.1.10']
async def test_bridge_discovered(hass, mock_bridge):
"""Bridge discovery."""
assert await async_setup_component(hass, hue.DOMAIN, {})
await hass.helpers.discovery.async_discover(SERVICE_HUE, {
'host': '192.168.1.10',
'serial': '1234567',
})
await hass.async_block_till_done()
assert len(mock_bridge.mock_calls) == 1
assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10'
async def test_bridge_configure_and_discovered(hass, mock_bridge):
"""Bridge is in the config file, then we discover it."""
assert await async_setup_component(hass, hue.DOMAIN, {
hue.DOMAIN: {
hue.CONF_BRIDGES: {
hue.CONF_HOST: '192.168.1.10'
}
}
})
assert len(mock_bridge.mock_calls) == 1
assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10'
hass.data[hue.DOMAIN] = {'192.168.1.10': {}}
mock_bridge.reset_mock()
await hass.helpers.discovery.async_discover(SERVICE_HUE, {
'host': '192.168.1.10',
'serial': '1234567',
})
await hass.async_block_till_done()
assert len(mock_bridge.mock_calls) == 0
async def test_setup_no_host(hass, aioclient_mock):
"""Check we call discovery if domain specified but no bridges."""
aioclient_mock.get(hue.API_NUPNP, json=[])
result = await async_setup_component(
hass, hue.DOMAIN, {hue.DOMAIN: {}})
assert result
assert len(aioclient_mock.mock_calls) == 1

View File

@ -5,6 +5,7 @@ from unittest.mock import patch, MagicMock
import pytest
from homeassistant import config_entries
from homeassistant.bootstrap import async_setup_component
from homeassistant.components import discovery
from homeassistant.util.dt import utcnow
@ -44,13 +45,12 @@ def netdisco_mock():
yield
@asyncio.coroutine
def mock_discovery(hass, discoveries, config=BASE_CONFIG):
async def mock_discovery(hass, discoveries, config=BASE_CONFIG):
"""Helper to mock discoveries."""
result = yield from async_setup_component(hass, 'discovery', config)
result = await async_setup_component(hass, 'discovery', config)
assert result
yield from hass.async_start()
await hass.async_start()
with patch.object(discovery, '_discover', discoveries), \
patch('homeassistant.components.discovery.async_discover',
@ -59,8 +59,8 @@ def mock_discovery(hass, discoveries, config=BASE_CONFIG):
return_value=mock_coro()) as mock_platform:
async_fire_time_changed(hass, utcnow())
# Work around an issue where our loop.call_soon not get caught
yield from hass.async_block_till_done()
yield from hass.async_block_till_done()
await hass.async_block_till_done()
await hass.async_block_till_done()
return mock_discover, mock_platform
@ -154,3 +154,25 @@ def test_load_component_hassio(hass):
yield from mock_discovery(hass, discover)
assert mock_hassio.called
async def test_discover_config_flow(hass):
"""Test discovery triggering a config flow."""
discovery_info = {
'hello': 'world'
}
def discover(netdisco):
"""Fake discovery."""
return [('mock-service', discovery_info)]
with patch.dict(discovery.CONFIG_ENTRY_HANDLERS, {
'mock-service': 'mock-component'}), patch(
'homeassistant.config_entries.FlowManager.async_init') as m_init:
await mock_discovery(hass, discover)
assert len(m_init.mock_calls) == 1
args, kwargs = m_init.mock_calls[0][1:]
assert args == ('mock-component',)
assert kwargs['source'] == config_entries.SOURCE_DISCOVERY
assert kwargs['data'] == discovery_info