mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Convert Hue to always use config entries (#13034)
This commit is contained in:
parent
1ae8b6ee08
commit
184f2be83e
@ -13,6 +13,7 @@ import os
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
@ -40,6 +41,10 @@ SERVICE_DECONZ = 'deconz'
|
|||||||
SERVICE_DAIKIN = 'daikin'
|
SERVICE_DAIKIN = 'daikin'
|
||||||
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
|
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
|
||||||
|
|
||||||
|
CONFIG_ENTRY_HANDLERS = {
|
||||||
|
SERVICE_HUE: 'hue',
|
||||||
|
}
|
||||||
|
|
||||||
SERVICE_HANDLERS = {
|
SERVICE_HANDLERS = {
|
||||||
SERVICE_HASS_IOS_APP: ('ios', None),
|
SERVICE_HASS_IOS_APP: ('ios', None),
|
||||||
SERVICE_NETGEAR: ('device_tracker', None),
|
SERVICE_NETGEAR: ('device_tracker', None),
|
||||||
@ -51,7 +56,6 @@ SERVICE_HANDLERS = {
|
|||||||
SERVICE_WINK: ('wink', None),
|
SERVICE_WINK: ('wink', None),
|
||||||
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
|
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
|
||||||
SERVICE_TELLDUSLIVE: ('tellduslive', None),
|
SERVICE_TELLDUSLIVE: ('tellduslive', None),
|
||||||
SERVICE_HUE: ('hue', None),
|
|
||||||
SERVICE_DECONZ: ('deconz', None),
|
SERVICE_DECONZ: ('deconz', None),
|
||||||
SERVICE_DAIKIN: ('daikin', None),
|
SERVICE_DAIKIN: ('daikin', None),
|
||||||
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
|
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
|
||||||
@ -105,6 +109,20 @@ async def async_setup(hass, config):
|
|||||||
logger.info("Ignoring service: %s %s", service, info)
|
logger.info("Ignoring service: %s %s", service, info)
|
||||||
return
|
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)
|
comp_plat = SERVICE_HANDLERS.get(service)
|
||||||
|
|
||||||
# We do not know how to handle this 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)
|
logger.info("Unknown service discovered: %s %s", service, info)
|
||||||
return
|
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)
|
logger.info("Found new service: %s %s", service, info)
|
||||||
|
|
||||||
component, platform = comp_plat
|
component, platform = comp_plat
|
||||||
|
@ -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
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/hue/
|
https://home-assistant.io/components/hue/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
|
|
||||||
import async_timeout
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
|
||||||
from homeassistant.components.discovery import SERVICE_HUE
|
|
||||||
from homeassistant.const import CONF_FILENAME, CONF_HOST
|
from homeassistant.const import CONF_FILENAME, CONF_HOST
|
||||||
import homeassistant.helpers.config_validation as cv
|
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||||
from homeassistant.helpers import discovery, aiohttp_client
|
|
||||||
from homeassistant import config_entries
|
from .const import DOMAIN, API_NUPNP
|
||||||
from homeassistant.util.json import save_json
|
from .bridge import HueBridge
|
||||||
|
# Loading the config flow file will register the flow
|
||||||
|
from .config_flow import configured_hosts
|
||||||
|
|
||||||
REQUIREMENTS = ['aiohue==1.3.0']
|
REQUIREMENTS = ['aiohue==1.3.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "hue"
|
|
||||||
SERVICE_HUE_SCENE = "hue_activate_scene"
|
|
||||||
API_NUPNP = 'https://www.meethue.com/api/nupnp'
|
|
||||||
|
|
||||||
CONF_BRIDGES = "bridges"
|
CONF_BRIDGES = "bridges"
|
||||||
|
|
||||||
CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
|
CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
|
||||||
@ -42,6 +34,7 @@ DEFAULT_ALLOW_HUE_GROUPS = True
|
|||||||
BRIDGE_CONFIG_SCHEMA = vol.Schema({
|
BRIDGE_CONFIG_SCHEMA = vol.Schema({
|
||||||
# Validate as IP address and then convert back to a string.
|
# Validate as IP address and then convert back to a string.
|
||||||
vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.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_FILENAME, default=PHUE_CONFIG_FILE): cv.string,
|
||||||
vol.Optional(CONF_ALLOW_UNREACHABLE,
|
vol.Optional(CONF_ALLOW_UNREACHABLE,
|
||||||
default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean,
|
default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean,
|
||||||
@ -56,19 +49,6 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
}),
|
}),
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, 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.
|
|
||||||
|
|
||||||

|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up the Hue platform."""
|
"""Set up the Hue platform."""
|
||||||
@ -76,20 +56,8 @@ async def async_setup(hass, config):
|
|||||||
if conf is None:
|
if conf is None:
|
||||||
conf = {}
|
conf = {}
|
||||||
|
|
||||||
if DOMAIN not in hass.data:
|
hass.data[DOMAIN] = {}
|
||||||
hass.data[DOMAIN] = {}
|
configured = configured_hosts(hass)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
# User has configured bridges
|
# User has configured bridges
|
||||||
if CONF_BRIDGES in conf:
|
if CONF_BRIDGES in conf:
|
||||||
@ -103,12 +71,19 @@ async def async_setup(hass, config):
|
|||||||
async with websession.get(API_NUPNP) as req:
|
async with websession.get(API_NUPNP) as req:
|
||||||
hosts = await req.json()
|
hosts = await req.json()
|
||||||
|
|
||||||
# Run through config schema to populate defaults
|
bridges = []
|
||||||
bridges = [BRIDGE_CONFIG_SCHEMA({
|
for entry in hosts:
|
||||||
CONF_HOST: entry['internalipaddress'],
|
# Filter out already configured hosts
|
||||||
CONF_FILENAME: '.hue_{}.conf'.format(entry['id']),
|
if entry['internalipaddress'] in configured:
|
||||||
}) for entry in hosts]
|
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:
|
else:
|
||||||
# Component not specified in config, we're loaded via discovery
|
# Component not specified in config, we're loaded via discovery
|
||||||
bridges = []
|
bridges = []
|
||||||
@ -116,277 +91,43 @@ async def async_setup(hass, config):
|
|||||||
if not bridges:
|
if not bridges:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
await asyncio.wait([
|
for bridge_conf in bridges:
|
||||||
async_setup_bridge(
|
host = bridge_conf[CONF_HOST]
|
||||||
hass, bridge[CONF_HOST], bridge[CONF_FILENAME],
|
|
||||||
bridge[CONF_ALLOW_UNREACHABLE], bridge[CONF_ALLOW_HUE_GROUPS]
|
# Store config in hass.data so the config entry can find it
|
||||||
) for bridge in bridges
|
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
|
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):
|
async def async_setup_entry(hass, entry):
|
||||||
"""Set up a bridge for a config entry."""
|
"""Set up a bridge from a config entry."""
|
||||||
await async_setup_bridge(hass, entry.data['host'],
|
host = entry.data['host']
|
||||||
username=entry.data['username'])
|
config = hass.data[DOMAIN].get(host)
|
||||||
return True
|
|
||||||
|
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()
|
||||||
|
143
homeassistant/components/hue/bridge.py
Normal file
143
homeassistant/components/hue/bridge.py
Normal 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
|
235
homeassistant/components/hue/config_flow.py
Normal file
235
homeassistant/components/hue/config_flow.py
Normal 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,
|
||||||
|
}
|
||||||
|
)
|
6
homeassistant/components/hue/const.py
Normal file
6
homeassistant/components/hue/const.py
Normal 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'
|
14
homeassistant/components/hue/errors.py
Normal file
14
homeassistant/components/hue/errors.py
Normal 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."""
|
@ -20,7 +20,10 @@
|
|||||||
"abort": {
|
"abort": {
|
||||||
"discover_timeout": "Unable to discover Hue bridges",
|
"discover_timeout": "Unable to discover Hue bridges",
|
||||||
"no_bridges": "No Philips Hue bridges discovered",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -384,7 +384,7 @@ class FlowManager:
|
|||||||
handler = HANDLERS.get(domain)
|
handler = HANDLERS.get(domain)
|
||||||
|
|
||||||
if handler is None:
|
if handler is None:
|
||||||
raise self.hass.helpers.UnknownHandler
|
raise UnknownHandler
|
||||||
|
|
||||||
# Make sure requirements and dependencies of component are resolved
|
# Make sure requirements and dependencies of component are resolved
|
||||||
await async_process_deps_reqs(
|
await async_process_deps_reqs(
|
||||||
|
@ -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
|
|
@ -1,99 +1,57 @@
|
|||||||
"""Test Hue bridge."""
|
"""Test Hue bridge."""
|
||||||
import asyncio
|
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import aiohue
|
from homeassistant.components.hue import bridge, errors
|
||||||
import pytest
|
|
||||||
|
|
||||||
from homeassistant.components import hue
|
|
||||||
|
|
||||||
from tests.common import mock_coro
|
from tests.common import mock_coro
|
||||||
|
|
||||||
|
|
||||||
class MockBridge(hue.HueBridge):
|
async def test_bridge_setup():
|
||||||
"""Class that sets default for constructor."""
|
"""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',
|
with patch.object(bridge, 'get_bridge', return_value=mock_coro(api)):
|
||||||
username=None, **kwargs):
|
assert await hue_bridge.async_setup() is True
|
||||||
"""Initialize a mock bridge."""
|
|
||||||
super().__init__(host, hass, filename, username, **kwargs)
|
|
||||||
|
|
||||||
|
assert hue_bridge.api is api
|
||||||
@pytest.fixture
|
assert len(hass.helpers.discovery.async_load_platform.mock_calls) == 1
|
||||||
def mock_request():
|
assert hass.helpers.discovery.async_load_platform.mock_calls[0][1][2] == {
|
||||||
"""Mock configurator.async_request_config."""
|
'host': '1.2.3.4'
|
||||||
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 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
|
||||||
|
@ -1,28 +1,29 @@
|
|||||||
"""Tests for Philips Hue config flow."""
|
"""Tests for Philips Hue config flow."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from unittest.mock import patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import aiohue
|
import aiohue
|
||||||
import pytest
|
import pytest
|
||||||
import voluptuous as vol
|
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
|
from tests.common import MockConfigEntry, mock_coro
|
||||||
|
|
||||||
|
|
||||||
async def test_flow_works(hass, aioclient_mock):
|
async def test_flow_works(hass, aioclient_mock):
|
||||||
"""Test config flow ."""
|
"""Test config flow ."""
|
||||||
aioclient_mock.get(hue.API_NUPNP, json=[
|
aioclient_mock.get(const.API_NUPNP, json=[
|
||||||
{'internalipaddress': '1.2.3.4', 'id': 'bla'}
|
{'internalipaddress': '1.2.3.4', 'id': 'bla'}
|
||||||
])
|
])
|
||||||
|
|
||||||
flow = hue.HueFlowHandler()
|
flow = config_flow.HueFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
await flow.async_step_init()
|
await flow.async_step_init()
|
||||||
|
|
||||||
with patch('aiohue.Bridge') as mock_bridge:
|
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
|
mock_bridge.host = host
|
||||||
return mock_bridge
|
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):
|
async def test_flow_no_discovered_bridges(hass, aioclient_mock):
|
||||||
"""Test config flow discovers no bridges."""
|
"""Test config flow discovers no bridges."""
|
||||||
aioclient_mock.get(hue.API_NUPNP, json=[])
|
aioclient_mock.get(const.API_NUPNP, json=[])
|
||||||
flow = hue.HueFlowHandler()
|
flow = config_flow.HueFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
result = await flow.async_step_init()
|
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):
|
async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock):
|
||||||
"""Test config flow discovers only already configured bridges."""
|
"""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'}
|
{'internalipaddress': '1.2.3.4', 'id': 'bla'}
|
||||||
])
|
])
|
||||||
MockConfigEntry(domain='hue', data={
|
MockConfigEntry(domain='hue', data={
|
||||||
'host': '1.2.3.4'
|
'host': '1.2.3.4'
|
||||||
}).add_to_hass(hass)
|
}).add_to_hass(hass)
|
||||||
flow = hue.HueFlowHandler()
|
flow = config_flow.HueFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
result = await flow.async_step_init()
|
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):
|
async def test_flow_one_bridge_discovered(hass, aioclient_mock):
|
||||||
"""Test config flow discovers one bridge."""
|
"""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'}
|
{'internalipaddress': '1.2.3.4', 'id': 'bla'}
|
||||||
])
|
])
|
||||||
flow = hue.HueFlowHandler()
|
flow = config_flow.HueFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
result = await flow.async_step_init()
|
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):
|
async def test_flow_two_bridges_discovered(hass, aioclient_mock):
|
||||||
"""Test config flow discovers two bridges."""
|
"""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': '1.2.3.4', 'id': 'bla'},
|
||||||
{'internalipaddress': '5.6.7.8', 'id': 'beer'}
|
{'internalipaddress': '5.6.7.8', 'id': 'beer'}
|
||||||
])
|
])
|
||||||
flow = hue.HueFlowHandler()
|
flow = config_flow.HueFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
result = await flow.async_step_init()
|
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):
|
async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock):
|
||||||
"""Test config flow discovers two bridges."""
|
"""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': '1.2.3.4', 'id': 'bla'},
|
||||||
{'internalipaddress': '5.6.7.8', 'id': 'beer'}
|
{'internalipaddress': '5.6.7.8', 'id': 'beer'}
|
||||||
])
|
])
|
||||||
MockConfigEntry(domain='hue', data={
|
MockConfigEntry(domain='hue', data={
|
||||||
'host': '1.2.3.4'
|
'host': '1.2.3.4'
|
||||||
}).add_to_hass(hass)
|
}).add_to_hass(hass)
|
||||||
flow = hue.HueFlowHandler()
|
flow = config_flow.HueFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
result = await flow.async_step_init()
|
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):
|
async def test_flow_timeout_discovery(hass):
|
||||||
"""Test config flow ."""
|
"""Test config flow ."""
|
||||||
flow = hue.HueFlowHandler()
|
flow = config_flow.HueFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
with patch('aiohue.discovery.discover_nupnp',
|
with patch('aiohue.discovery.discover_nupnp',
|
||||||
@ -138,7 +139,7 @@ async def test_flow_timeout_discovery(hass):
|
|||||||
|
|
||||||
async def test_flow_link_timeout(hass):
|
async def test_flow_link_timeout(hass):
|
||||||
"""Test config flow ."""
|
"""Test config flow ."""
|
||||||
flow = hue.HueFlowHandler()
|
flow = config_flow.HueFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
with patch('aiohue.Bridge.create_user',
|
with patch('aiohue.Bridge.create_user',
|
||||||
@ -148,13 +149,13 @@ async def test_flow_link_timeout(hass):
|
|||||||
assert result['type'] == 'form'
|
assert result['type'] == 'form'
|
||||||
assert result['step_id'] == 'link'
|
assert result['step_id'] == 'link'
|
||||||
assert result['errors'] == {
|
assert result['errors'] == {
|
||||||
'base': 'register_failed'
|
'base': 'linking'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_flow_link_button_not_pressed(hass):
|
async def test_flow_link_button_not_pressed(hass):
|
||||||
"""Test config flow ."""
|
"""Test config flow ."""
|
||||||
flow = hue.HueFlowHandler()
|
flow = config_flow.HueFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
with patch('aiohue.Bridge.create_user',
|
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):
|
async def test_flow_link_unknown_host(hass):
|
||||||
"""Test config flow ."""
|
"""Test config flow ."""
|
||||||
flow = hue.HueFlowHandler()
|
flow = config_flow.HueFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
with patch('aiohue.Bridge.create_user',
|
with patch('aiohue.Bridge.create_user',
|
||||||
@ -180,5 +181,175 @@ async def test_flow_link_unknown_host(hass):
|
|||||||
assert result['type'] == 'form'
|
assert result['type'] == 'form'
|
||||||
assert result['step_id'] == 'link'
|
assert result['step_id'] == 'link'
|
||||||
assert result['errors'] == {
|
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
|
||||||
|
169
tests/components/hue/test_init.py
Normal file
169
tests/components/hue/test_init.py
Normal 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
|
@ -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
|
|
@ -5,6 +5,7 @@ from unittest.mock import patch, MagicMock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
from homeassistant.bootstrap import async_setup_component
|
from homeassistant.bootstrap import async_setup_component
|
||||||
from homeassistant.components import discovery
|
from homeassistant.components import discovery
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
@ -44,13 +45,12 @@ def netdisco_mock():
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def mock_discovery(hass, discoveries, config=BASE_CONFIG):
|
||||||
def mock_discovery(hass, discoveries, config=BASE_CONFIG):
|
|
||||||
"""Helper to mock discoveries."""
|
"""Helper to mock discoveries."""
|
||||||
result = yield from async_setup_component(hass, 'discovery', config)
|
result = await async_setup_component(hass, 'discovery', config)
|
||||||
assert result
|
assert result
|
||||||
|
|
||||||
yield from hass.async_start()
|
await hass.async_start()
|
||||||
|
|
||||||
with patch.object(discovery, '_discover', discoveries), \
|
with patch.object(discovery, '_discover', discoveries), \
|
||||||
patch('homeassistant.components.discovery.async_discover',
|
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:
|
return_value=mock_coro()) as mock_platform:
|
||||||
async_fire_time_changed(hass, utcnow())
|
async_fire_time_changed(hass, utcnow())
|
||||||
# Work around an issue where our loop.call_soon not get caught
|
# Work around an issue where our loop.call_soon not get caught
|
||||||
yield from hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
yield from hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
return mock_discover, mock_platform
|
return mock_discover, mock_platform
|
||||||
|
|
||||||
@ -154,3 +154,25 @@ def test_load_component_hassio(hass):
|
|||||||
yield from mock_discovery(hass, discover)
|
yield from mock_discovery(hass, discover)
|
||||||
|
|
||||||
assert mock_hassio.called
|
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user