Refactor Hue: phue -> aiohue (#13043)

* phue -> aiohue

* Clean up

* Fix config

* Address comments

* Typo

* Fix rebase error

* Mark light as unavailable when bridge is disconnected

* Tests

* Make Throttle work with double delay and async

* Rework update logic

* Don't resolve host to IP

* Clarify comment

* No longer do unnecessary updates

* Add more doc

* Another comment update

* Wrap up tests

* Lint

* Fix tests

* PyLint does not like mix 'n match async and coroutine

* Lint

* Update aiohue to 1.2

* Lint

* Fix await MagicMock
This commit is contained in:
Paulus Schoutsen 2018-03-16 20:27:05 -07:00 committed by GitHub
parent d78e75db66
commit 5a9013cda5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1289 additions and 1485 deletions

View file

@ -6,7 +6,6 @@ Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered.
Knows which components handle certain types, will make sure they are Knows which components handle certain types, will make sure they are
loaded before the EVENT_PLATFORM_DISCOVERED is fired. loaded before the EVENT_PLATFORM_DISCOVERED is fired.
""" """
import asyncio
import json import json
from datetime import timedelta from datetime import timedelta
import logging import logging
@ -84,8 +83,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine async def async_setup(hass, config):
def async_setup(hass, config):
"""Start a discovery service.""" """Start a discovery service."""
from netdisco.discovery import NetworkDiscovery from netdisco.discovery import NetworkDiscovery
@ -99,8 +97,7 @@ def async_setup(hass, config):
# Platforms ignore by config # Platforms ignore by config
ignored_platforms = config[DOMAIN][CONF_IGNORE] ignored_platforms = config[DOMAIN][CONF_IGNORE]
@asyncio.coroutine async def new_service_found(service, info):
def new_service_found(service, info):
"""Handle a new service if one is found.""" """Handle a new service if one is found."""
if service in ignored_platforms: if service in ignored_platforms:
logger.info("Ignoring service: %s %s", service, info) logger.info("Ignoring service: %s %s", service, info)
@ -124,15 +121,14 @@ def async_setup(hass, config):
component, platform = comp_plat component, platform = comp_plat
if platform is None: if platform is None:
yield from async_discover(hass, service, info, component, config) await async_discover(hass, service, info, component, config)
else: else:
yield from async_load_platform( await async_load_platform(
hass, component, platform, info, config) hass, component, platform, info, config)
@asyncio.coroutine async def scan_devices(now):
def scan_devices(now):
"""Scan for devices.""" """Scan for devices."""
results = yield from hass.async_add_job(_discover, netdisco) results = await hass.async_add_job(_discover, netdisco)
for result in results: for result in results:
hass.async_add_job(new_service_found(*result)) hass.async_add_job(new_service_found(*result))

View file

@ -6,22 +6,22 @@ https://home-assistant.io/components/hue/
""" """
import asyncio import asyncio
import json import json
from functools import partial import ipaddress
import logging import logging
import os import os
import socket
import async_timeout import async_timeout
import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.discovery import SERVICE_HUE 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 import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery, aiohttp_client from homeassistant.helpers import discovery, aiohttp_client
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.util.json import save_json
REQUIREMENTS = ['phue==1.0', 'aiohue==0.3.0'] REQUIREMENTS = ['aiohue==1.2.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -36,26 +36,23 @@ DEFAULT_ALLOW_UNREACHABLE = False
PHUE_CONFIG_FILE = 'phue.conf' PHUE_CONFIG_FILE = 'phue.conf'
CONF_ALLOW_IN_EMULATED_HUE = "allow_in_emulated_hue"
DEFAULT_ALLOW_IN_EMULATED_HUE = True
CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" CONF_ALLOW_HUE_GROUPS = "allow_hue_groups"
DEFAULT_ALLOW_HUE_GROUPS = True DEFAULT_ALLOW_HUE_GROUPS = True
BRIDGE_CONFIG_SCHEMA = vol.Schema([{ BRIDGE_CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_HOST): cv.string, # Validate as IP address and then convert back to a string.
vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string),
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,
vol.Optional(CONF_ALLOW_IN_EMULATED_HUE,
default=DEFAULT_ALLOW_IN_EMULATED_HUE): cv.boolean,
vol.Optional(CONF_ALLOW_HUE_GROUPS, vol.Optional(CONF_ALLOW_HUE_GROUPS,
default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean, default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean,
}]) })
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Optional(CONF_BRIDGES): BRIDGE_CONFIG_SCHEMA, vol.Optional(CONF_BRIDGES):
vol.All(cv.ensure_list, [BRIDGE_CONFIG_SCHEMA]),
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -73,7 +70,7 @@ Press the button on the bridge to register Philips Hue with Home Assistant.
""" """
def setup(hass, config): async def async_setup(hass, config):
"""Set up the Hue platform.""" """Set up the Hue platform."""
conf = config.get(DOMAIN) conf = config.get(DOMAIN)
if conf is None: if conf is None:
@ -82,135 +79,130 @@ def setup(hass, config):
if DOMAIN not in hass.data: if DOMAIN not in hass.data:
hass.data[DOMAIN] = {} hass.data[DOMAIN] = {}
discovery.listen( async def async_bridge_discovered(service, discovery_info):
hass, """Dispatcher for Hue discovery events."""
SERVICE_HUE, # Ignore emulated hue
lambda service, discovery_info: if "HASS Bridge" in discovery_info.get('name', ''):
bridge_discovered(hass, service, discovery_info)) 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:
bridges = conf[CONF_BRIDGES] bridges = conf[CONF_BRIDGES]
# Component is part of config but no bridges specified, discover. # Component is part of config but no bridges specified, discover.
elif DOMAIN in config: elif DOMAIN in config:
# discover from nupnp # discover from nupnp
hosts = requests.get(API_NUPNP).json() websession = aiohttp_client.async_get_clientsession(hass)
bridges = [{
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_HOST: entry['internalipaddress'],
CONF_FILENAME: '.hue_{}.conf'.format(entry['id']), CONF_FILENAME: '.hue_{}.conf'.format(entry['id']),
} for entry in hosts] }) for entry in hosts]
else: else:
# Component not specified in config, we're loaded via discovery # Component not specified in config, we're loaded via discovery
bridges = [] bridges = []
for bridge in bridges: if not bridges:
filename = bridge.get(CONF_FILENAME) return True
allow_unreachable = bridge.get(CONF_ALLOW_UNREACHABLE)
allow_in_emulated_hue = bridge.get(CONF_ALLOW_IN_EMULATED_HUE)
allow_hue_groups = bridge.get(CONF_ALLOW_HUE_GROUPS)
host = bridge.get(CONF_HOST) await asyncio.wait([
async_setup_bridge(
if host is None: hass, bridge[CONF_HOST], bridge[CONF_FILENAME],
host = _find_host_from_config(hass, filename) bridge[CONF_ALLOW_UNREACHABLE], bridge[CONF_ALLOW_HUE_GROUPS]
) for bridge in bridges
if host is None: ])
_LOGGER.error("No host found in configuration")
return False
setup_bridge(host, hass, filename, allow_unreachable,
allow_in_emulated_hue, allow_hue_groups)
return True return True
def bridge_discovered(hass, service, discovery_info): async def async_setup_bridge(
"""Dispatcher for Hue discovery events.""" hass, host, filename=None,
if "HASS Bridge" in discovery_info.get('name', ''): allow_unreachable=DEFAULT_ALLOW_UNREACHABLE,
return allow_hue_groups=DEFAULT_ALLOW_HUE_GROUPS,
host = discovery_info.get('host')
serial = discovery_info.get('serial')
filename = 'phue-{}.conf'.format(serial)
setup_bridge(host, hass, filename)
def setup_bridge(host, hass, filename=None, allow_unreachable=False,
allow_in_emulated_hue=True, allow_hue_groups=True,
username=None): username=None):
"""Set up a given Hue bridge.""" """Set up a given Hue bridge."""
assert filename or username, 'Need to pass at least a username or filename'
# Only register a device once # Only register a device once
if socket.gethostbyname(host) in hass.data[DOMAIN]: if host in hass.data[DOMAIN]:
return 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, bridge = HueBridge(host, hass, filename, username, allow_unreachable,
allow_in_emulated_hue, allow_hue_groups) allow_hue_groups)
bridge.setup() await bridge.async_setup()
hass.data[DOMAIN][host] = bridge
def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE): def _find_username_from_config(hass, filename):
"""Attempt to detect host based on existing configuration.""" """Load username from config."""
path = hass.config.path(filename) path = hass.config.path(filename)
if not os.path.isfile(path): if not os.path.isfile(path):
return None return None
try:
with open(path) as inp: with open(path) as inp:
return next(iter(json.load(inp).keys())) return list(json.load(inp).values())[0]['username']
except (ValueError, AttributeError, StopIteration):
# ValueError if can't parse as JSON
# AttributeError if JSON value is not a dict
# StopIteration if no keys
return None
class HueBridge(object): class HueBridge(object):
"""Manages a single Hue bridge.""" """Manages a single Hue bridge."""
def __init__(self, host, hass, filename, username, allow_unreachable=False, def __init__(self, host, hass, filename, username,
allow_in_emulated_hue=True, allow_hue_groups=True): allow_unreachable=False, allow_groups=True):
"""Initialize the system.""" """Initialize the system."""
self.host = host self.host = host
self.bridge_id = socket.gethostbyname(host)
self.hass = hass self.hass = hass
self.filename = filename self.filename = filename
self.username = username self.username = username
self.allow_unreachable = allow_unreachable self.allow_unreachable = allow_unreachable
self.allow_in_emulated_hue = allow_in_emulated_hue self.allow_groups = allow_groups
self.allow_hue_groups = allow_hue_groups
self.available = True self.available = True
self.bridge = None
self.lights = {}
self.lightgroups = {}
self.configured = False
self.config_request_id = None self.config_request_id = None
self.api = None
hass.data[DOMAIN][self.bridge_id] = self async def async_setup(self):
def setup(self):
"""Set up a phue bridge based on host parameter.""" """Set up a phue bridge based on host parameter."""
import phue import aiohue
api = aiohue.Bridge(
self.host,
username=self.username,
websession=aiohttp_client.async_get_clientsession(self.hass)
)
try: try:
kwargs = {} with async_timeout.timeout(5):
if self.username is not None: # Initialize bridge and validate our username
kwargs['username'] = self.username if not self.username:
if self.filename is not None: await api.create_user('home-assistant')
kwargs['config_file_path'] = \ await api.initialize()
self.hass.config.path(self.filename) except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized):
self.bridge = phue.Bridge(self.host, **kwargs) _LOGGER.warning("Connected to Hue at %s but not registered.",
except OSError: # Wrong host was given self.host)
self.async_request_configuration()
return
except (asyncio.TimeoutError, aiohue.RequestError):
_LOGGER.error("Error connecting to the Hue bridge at %s", _LOGGER.error("Error connecting to the Hue bridge at %s",
self.host) self.host)
return return
except phue.PhueRegistrationException: except aiohue.AiohueException:
_LOGGER.warning("Connected to Hue at %s but not registered.", _LOGGER.exception('Unknown Hue linking error occurred')
self.host) self.async_request_configuration()
self.request_configuration()
return return
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unknown error connecting with Hue bridge at %s", _LOGGER.exception("Unknown error connecting with Hue bridge at %s",
@ -221,57 +213,77 @@ class HueBridge(object):
if self.config_request_id: if self.config_request_id:
request_id = self.config_request_id request_id = self.config_request_id
self.config_request_id = None self.config_request_id = None
configurator = self.hass.components.configurator self.hass.components.configurator.async_request_done(request_id)
configurator.request_done(request_id)
self.configured = True self.username = api.username
discovery.load_platform( # 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, self.hass, 'light', DOMAIN,
{'bridge_id': self.bridge_id}) {'host': self.host}))
# create a service for calling run_scene directly on the bridge, self.hass.services.async_register(
# used to simplify automation rules. DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene,
def hue_activate_scene(call):
"""Service to call directly into bridge to set scenes."""
group_name = call.data[ATTR_GROUP_NAME]
scene_name = call.data[ATTR_SCENE_NAME]
self.bridge.run_scene(group_name, scene_name)
self.hass.services.register(
DOMAIN, SERVICE_HUE_SCENE, hue_activate_scene,
schema=SCENE_SCHEMA) schema=SCENE_SCHEMA)
def request_configuration(self): @callback
def async_request_configuration(self):
"""Request configuration steps from the user.""" """Request configuration steps from the user."""
configurator = self.hass.components.configurator configurator = self.hass.components.configurator
# We got an error if this method is called while we are configuring # We got an error if this method is called while we are configuring
if self.config_request_id: if self.config_request_id:
configurator.notify_errors( configurator.async_notify_errors(
self.config_request_id, self.config_request_id,
"Failed to register, please try again.") "Failed to register, please try again.")
return return
self.config_request_id = configurator.request_config( async def config_callback(data):
"Philips Hue", """Callback for configurator data."""
lambda data: self.setup(), await self.async_setup()
self.config_request_id = configurator.async_request_config(
"Philips Hue", config_callback,
description=CONFIG_INSTRUCTIONS, description=CONFIG_INSTRUCTIONS,
entity_picture="/static/images/logo_philips_hue.png", entity_picture="/static/images/logo_philips_hue.png",
submit_caption="I have pressed the button" submit_caption="I have pressed the button"
) )
def get_api(self): async def hue_activate_scene(self, call, updated=False):
"""Return the full api dictionary from phue.""" """Service to call directly into bridge to set scenes."""
return self.bridge.get_api() group_name = call.data[ATTR_GROUP_NAME]
scene_name = call.data[ATTR_SCENE_NAME]
def set_light(self, light_id, command): group = next(
"""Adjust properties of one or more lights. See phue for details.""" (group for group in self.api.groups.values()
return self.bridge.set_light(light_id, command) if group.name == group_name), None)
def set_group(self, light_id, command): scene_id = next(
"""Change light settings for a group. See phue for detail.""" (scene.id for scene in self.api.scenes.values()
return self.bridge.set_group(light_id, command) 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) @config_entries.HANDLERS.register(DOMAIN)
@ -374,7 +386,6 @@ class HueFlowHandler(config_entries.ConfigFlowHandler):
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 for a config entry."""
await hass.async_add_job(partial( await async_setup_bridge(hass, entry.data['host'],
setup_bridge, entry.data['host'], hass, username=entry.data['username'])
username=entry.data['username']))
return True return True

View file

@ -8,31 +8,23 @@ import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
import random import random
import re
import socket
import voluptuous as vol import async_timeout
import homeassistant.components.hue as hue import homeassistant.components.hue as hue
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR,
ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_COLORLOOP, EFFECT_RANDOM, ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_COLORLOOP, EFFECT_RANDOM,
FLASH_LONG, FLASH_SHORT, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR,
SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light) SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light)
from homeassistant.const import CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME
import homeassistant.helpers.config_validation as cv
import homeassistant.util as util
from homeassistant.util import yaml
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
DEPENDENCIES = ['hue'] DEPENDENCIES = ['hue']
SCAN_INTERVAL = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
SUPPORT_HUE_ON_OFF = (SUPPORT_FLASH | SUPPORT_TRANSITION) SUPPORT_HUE_ON_OFF = (SUPPORT_FLASH | SUPPORT_TRANSITION)
SUPPORT_HUE_DIMMABLE = (SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS) SUPPORT_HUE_DIMMABLE = (SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS)
SUPPORT_HUE_COLOR_TEMP = (SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP) SUPPORT_HUE_COLOR_TEMP = (SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP)
@ -48,244 +40,232 @@ SUPPORT_HUE = {
'Color temperature light': SUPPORT_HUE_COLOR_TEMP 'Color temperature light': SUPPORT_HUE_COLOR_TEMP
} }
ATTR_EMULATED_HUE_HIDDEN = 'emulated_hue_hidden'
ATTR_IS_HUE_GROUP = 'is_hue_group' ATTR_IS_HUE_GROUP = 'is_hue_group'
# Minimum Hue Bridge API version to support groups
# Legacy configuration, will be removed in 0.60 # 1.4.0 introduced extended group info
CONF_ALLOW_UNREACHABLE = 'allow_unreachable' # 1.12 introduced the state object for groups
DEFAULT_ALLOW_UNREACHABLE = False # 1.13 introduced "any_on" to group state objects
CONF_ALLOW_IN_EMULATED_HUE = 'allow_in_emulated_hue' GROUP_MIN_API_VERSION = (1, 13, 0)
DEFAULT_ALLOW_IN_EMULATED_HUE = True
CONF_ALLOW_HUE_GROUPS = 'allow_hue_groups'
DEFAULT_ALLOW_HUE_GROUPS = True
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_ALLOW_UNREACHABLE): cv.boolean,
vol.Optional(CONF_FILENAME): cv.string,
vol.Optional(CONF_ALLOW_IN_EMULATED_HUE): cv.boolean,
vol.Optional(CONF_ALLOW_HUE_GROUPS,
default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean,
})
MIGRATION_ID = 'light_hue_config_migration'
MIGRATION_TITLE = 'Philips Hue Configuration Migration'
MIGRATION_INSTRUCTIONS = """
Configuration for the Philips Hue component has changed; action required.
You have configured at least one bridge:
hue:
{config}
This configuration is deprecated, please check the
[Hue component](https://home-assistant.io/components/hue/) page for more
information.
"""
SIGNAL_CALLBACK = 'hue_light_callback_{}_{}'
def setup_platform(hass, config, add_devices, discovery_info=None): async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the Hue lights.""" """Set up the Hue lights."""
if discovery_info is None or 'bridge_id' not in discovery_info: if discovery_info is None:
return return
if config is not None and config: bridge = hass.data[hue.DOMAIN][discovery_info['host']]
# Legacy configuration, will be removed in 0.60 cur_lights = {}
config_str = yaml.dump([config]) cur_groups = {}
# Indent so it renders in a fixed-width font
config_str = re.sub('(?m)^', ' ', config_str)
hass.components.persistent_notification.async_create(
MIGRATION_INSTRUCTIONS.format(config=config_str),
title=MIGRATION_TITLE,
notification_id=MIGRATION_ID)
bridge_id = discovery_info['bridge_id'] api_version = tuple(
bridge = hass.data[hue.DOMAIN][bridge_id] int(v) for v in bridge.api.config.apiversion.split('.'))
unthrottled_update_lights(hass, bridge, add_devices)
allow_groups = bridge.allow_groups
if allow_groups and api_version < GROUP_MIN_API_VERSION:
_LOGGER.warning('Please update your Hue bridge to support groups')
allow_groups = False
# Hue updates all lights via a single API call.
#
# If we call a service to update 2 lights, we only want the API to be
# called once.
#
# The throttle decorator will return right away if a call is currently
# in progress. This means that if we are updating 2 lights, the first one
# is in the update method, the second one will skip it and assume the
# update went through and updates it's data, not good!
#
# The current mechanism will make sure that all lights will wait till
# the update call is done before writing their data to the state machine.
#
# An alternative approach would be to disable automatic polling by Home
# Assistant and take control ourselves. This works great for polling as now
# we trigger from 1 time update an update to all entities. However it gets
# tricky from inside async_turn_on and async_turn_off.
#
# If automatic polling is enabled, Home Assistant will call the entity
# update method after it is done calling all the services. This means that
# when we update, we know all commands have been processed. If we trigger
# the update from inside async_turn_on, the update will not capture the
# changes to the second entity until the next polling update because the
# throttle decorator will prevent the call.
progress = None
light_progress = set()
group_progress = set()
async def request_update(is_group, object_id):
"""Request an update.
We will only make 1 request to the server for updating at a time. If a
request is in progress, we will join the request that is in progress.
This approach is possible because should_poll=True. That means that
Home Assistant will ask lights for updates during a polling cycle or
after it has called a service.
We keep track of the lights that are waiting for the request to finish.
When new data comes in, we'll trigger an update for all non-waiting
lights. This covers the case where a service is called to enable 2
lights but in the meanwhile some other light has changed too.
"""
nonlocal progress
progress_set = group_progress if is_group else light_progress
progress_set.add(object_id)
if progress is not None:
return await progress
progress = asyncio.ensure_future(update_bridge())
result = await progress
progress = None
light_progress.clear()
group_progress.clear()
return result
async def update_bridge():
"""Update the values of the bridge.
Will update lights and, if enabled, groups from the bridge.
"""
tasks = []
tasks.append(async_update_items(
hass, bridge, async_add_devices, request_update,
False, cur_lights, light_progress
))
if allow_groups:
tasks.append(async_update_items(
hass, bridge, async_add_devices, request_update,
True, cur_groups, group_progress
))
await asyncio.wait(tasks)
await update_bridge()
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) async def async_update_items(hass, bridge, async_add_devices,
def update_lights(hass, bridge, add_devices): request_bridge_update, is_group, current,
"""Update the Hue light objects with latest info from the bridge.""" progress_waiting):
return unthrottled_update_lights(hass, bridge, add_devices) """Update either groups or lights from the bridge."""
import aiohue
if is_group:
def unthrottled_update_lights(hass, bridge, add_devices): api = bridge.api.groups
"""Update the lights (Internal version of update_lights).""" else:
import phue api = bridge.api.lights
if not bridge.configured:
return
try: try:
api = bridge.get_api() with async_timeout.timeout(4):
except phue.PhueRequestTimeout: await api.update()
_LOGGER.warning("Timeout trying to reach the bridge") except (asyncio.TimeoutError, aiohue.AiohueException):
bridge.available = False if not bridge.available:
return
except ConnectionRefusedError:
_LOGGER.error("The bridge refused the connection")
bridge.available = False
return
except socket.error:
# socket.error when we cannot reach Hue
_LOGGER.exception("Cannot reach the bridge")
bridge.available = False
return return
_LOGGER.error('Unable to reach bridge %s', bridge.host)
bridge.available = False
for light_id, light in current.items():
if light_id not in progress_waiting:
light.async_schedule_update_ha_state()
return
if not bridge.available:
_LOGGER.info('Reconnected to bridge %s', bridge.host)
bridge.available = True bridge.available = True
new_lights = process_lights( new_lights = []
hass, api, bridge,
lambda **kw: update_lights(hass, bridge, add_devices, **kw)) for item_id in api:
if bridge.allow_hue_groups: if item_id not in current:
new_lightgroups = process_groups( current[item_id] = HueLight(
hass, api, bridge, api[item_id], request_bridge_update, bridge, is_group)
lambda **kw: update_lights(hass, bridge, add_devices, **kw))
new_lights.extend(new_lightgroups) new_lights.append(current[item_id])
elif item_id not in progress_waiting:
current[item_id].async_schedule_update_ha_state()
if new_lights: if new_lights:
add_devices(new_lights) async_add_devices(new_lights)
def process_lights(hass, api, bridge, update_lights_cb):
"""Set up HueLight objects for all lights."""
api_lights = api.get('lights')
if not isinstance(api_lights, dict):
_LOGGER.error("Got unexpected result from Hue API")
return []
new_lights = []
for light_id, info in api_lights.items():
if light_id not in bridge.lights:
bridge.lights[light_id] = HueLight(
int(light_id), info, bridge,
update_lights_cb,
bridge.allow_unreachable,
bridge.allow_in_emulated_hue)
new_lights.append(bridge.lights[light_id])
else:
bridge.lights[light_id].info = info
hass.helpers.dispatcher.dispatcher_send(
SIGNAL_CALLBACK.format(
bridge.bridge_id,
bridge.lights[light_id].light_id))
return new_lights
def process_groups(hass, api, bridge, update_lights_cb):
"""Set up HueLight objects for all groups."""
api_groups = api.get('groups')
if not isinstance(api_groups, dict):
_LOGGER.error('Got unexpected result from Hue API')
return []
new_lights = []
for lightgroup_id, info in api_groups.items():
if 'state' not in info:
_LOGGER.warning(
"Group info does not contain state. Please update your hub")
return []
if lightgroup_id not in bridge.lightgroups:
bridge.lightgroups[lightgroup_id] = HueLight(
int(lightgroup_id), info, bridge,
update_lights_cb,
bridge.allow_unreachable,
bridge.allow_in_emulated_hue, True)
new_lights.append(bridge.lightgroups[lightgroup_id])
else:
bridge.lightgroups[lightgroup_id].info = info
hass.helpers.dispatcher.dispatcher_send(
SIGNAL_CALLBACK.format(
bridge.bridge_id,
bridge.lightgroups[lightgroup_id].light_id))
return new_lights
class HueLight(Light): class HueLight(Light):
"""Representation of a Hue light.""" """Representation of a Hue light."""
def __init__(self, light_id, info, bridge, update_lights_cb, def __init__(self, light, request_bridge_update, bridge, is_group=False):
allow_unreachable, allow_in_emulated_hue, is_group=False):
"""Initialize the light.""" """Initialize the light."""
self.light_id = light_id self.light = light
self.info = info self.async_request_bridge_update = request_bridge_update
self.bridge = bridge self.bridge = bridge
self.update_lights = update_lights_cb
self.allow_unreachable = allow_unreachable
self.is_group = is_group self.is_group = is_group
self.allow_in_emulated_hue = allow_in_emulated_hue
if is_group: if is_group:
self._command_func = self.bridge.set_group self.is_osram = False
self.is_philips = False
else: else:
self._command_func = self.bridge.set_light self.is_osram = light.manufacturername == 'OSRAM'
self.is_philips = light.manufacturername == 'Philips'
@property @property
def unique_id(self): def unique_id(self):
"""Return the ID of this Hue light.""" """Return the ID of this Hue light."""
return self.info.get('uniqueid') return self.light.uniqueid
@property @property
def name(self): def name(self):
"""Return the name of the Hue light.""" """Return the name of the Hue light."""
return self.info.get('name', DEVICE_DEFAULT_NAME) return self.light.name
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
if self.is_group: if self.is_group:
return self.info['action'].get('bri') return self.light.action.get('bri')
return self.info['state'].get('bri') return self.light.state.get('bri')
@property @property
def xy_color(self): def xy_color(self):
"""Return the XY color value.""" """Return the XY color value."""
if self.is_group: if self.is_group:
return self.info['action'].get('xy') return self.light.action.get('xy')
return self.info['state'].get('xy') return self.light.state.get('xy')
@property @property
def color_temp(self): def color_temp(self):
"""Return the CT color value.""" """Return the CT color value."""
if self.is_group: if self.is_group:
return self.info['action'].get('ct') return self.light.action.get('ct')
return self.info['state'].get('ct') return self.light.state.get('ct')
@property @property
def is_on(self): def is_on(self):
"""Return true if device is on.""" """Return true if device is on."""
if self.is_group: if self.is_group:
return self.info['state']['any_on'] return self.light.state['any_on']
return self.info['state']['on'] return self.light.state['on']
@property @property
def available(self): def available(self):
"""Return if light is available.""" """Return if light is available."""
return self.bridge.available and (self.is_group or return self.bridge.available and (self.is_group or
self.allow_unreachable or self.bridge.allow_unreachable or
self.info['state']['reachable']) self.light.state['reachable'])
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
return SUPPORT_HUE.get(self.info.get('type'), SUPPORT_HUE_EXTENDED) return SUPPORT_HUE.get(self.light.type, SUPPORT_HUE_EXTENDED)
@property @property
def effect_list(self): def effect_list(self):
"""Return the list of supported effects.""" """Return the list of supported effects."""
return [EFFECT_COLORLOOP, EFFECT_RANDOM] return [EFFECT_COLORLOOP, EFFECT_RANDOM]
def turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn the specified or all lights on.""" """Turn the specified or all lights on."""
command = {'on': True} command = {'on': True}
@ -293,7 +273,7 @@ class HueLight(Light):
command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10) command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10)
if ATTR_XY_COLOR in kwargs: if ATTR_XY_COLOR in kwargs:
if self.info.get('manufacturername') == 'OSRAM': if self.is_osram:
color_hue, sat = color_util.color_xy_to_hs( color_hue, sat = color_util.color_xy_to_hs(
*kwargs[ATTR_XY_COLOR]) *kwargs[ATTR_XY_COLOR])
command['hue'] = color_hue / 360 * 65535 command['hue'] = color_hue / 360 * 65535
@ -301,7 +281,7 @@ class HueLight(Light):
else: else:
command['xy'] = kwargs[ATTR_XY_COLOR] command['xy'] = kwargs[ATTR_XY_COLOR]
elif ATTR_RGB_COLOR in kwargs: elif ATTR_RGB_COLOR in kwargs:
if self.info.get('manufacturername') == 'OSRAM': if self.is_osram:
hsv = color_util.color_RGB_to_hsv( hsv = color_util.color_RGB_to_hsv(
*(int(val) for val in kwargs[ATTR_RGB_COLOR])) *(int(val) for val in kwargs[ATTR_RGB_COLOR]))
command['hue'] = hsv[0] / 360 * 65535 command['hue'] = hsv[0] / 360 * 65535
@ -336,12 +316,15 @@ class HueLight(Light):
elif effect == EFFECT_RANDOM: elif effect == EFFECT_RANDOM:
command['hue'] = random.randrange(0, 65535) command['hue'] = random.randrange(0, 65535)
command['sat'] = random.randrange(150, 254) command['sat'] = random.randrange(150, 254)
elif self.info.get('manufacturername') == 'Philips': elif self.is_philips:
command['effect'] = 'none' command['effect'] = 'none'
self._command_func(self.light_id, command) if self.is_group:
await self.light.set_action(**command)
else:
await self.light.set_state(**command)
def turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Turn the specified or all lights off.""" """Turn the specified or all lights off."""
command = {'on': False} command = {'on': False}
@ -359,27 +342,19 @@ class HueLight(Light):
else: else:
command['alert'] = 'none' command['alert'] = 'none'
self._command_func(self.light_id, command) if self.is_group:
await self.light.set_action(**command)
else:
await self.light.set_state(**command)
def update(self): async def async_update(self):
"""Synchronize state with bridge.""" """Synchronize state with bridge."""
self.update_lights(no_throttle=True) await self.async_request_bridge_update(self.is_group, self.light.id)
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the device state attributes.""" """Return the device state attributes."""
attributes = {} attributes = {}
if not self.allow_in_emulated_hue:
attributes[ATTR_EMULATED_HUE_HIDDEN] = \
not self.allow_in_emulated_hue
if self.is_group: if self.is_group:
attributes[ATTR_IS_HUE_GROUP] = self.is_group attributes[ATTR_IS_HUE_GROUP] = self.is_group
return attributes return attributes
@asyncio.coroutine
def async_added_to_hass(self):
"""Register update callback."""
dev_id = self.bridge.bridge_id, self.light_id
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_CALLBACK.format(*dev_id),
self.async_schedule_update_ha_state)

View file

@ -4,7 +4,6 @@ Support for MQTT discovery.
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/mqtt/#discovery https://home-assistant.io/components/mqtt/#discovery
""" """
import asyncio
import json import json
import logging import logging
import re import re
@ -35,19 +34,16 @@ ALLOWED_PLATFORMS = {
ALREADY_DISCOVERED = 'mqtt_discovered_components' ALREADY_DISCOVERED = 'mqtt_discovered_components'
@asyncio.coroutine async def async_start(hass, discovery_topic, hass_config):
def async_start(hass, discovery_topic, hass_config):
"""Initialize of MQTT Discovery.""" """Initialize of MQTT Discovery."""
# pylint: disable=unused-variable async def async_device_message_received(topic, payload, qos):
@asyncio.coroutine
def async_device_message_received(topic, payload, qos):
"""Process the received message.""" """Process the received message."""
match = TOPIC_MATCHER.match(topic) match = TOPIC_MATCHER.match(topic)
if not match: if not match:
return return
prefix_topic, component, node_id, object_id = match.groups() _prefix_topic, component, node_id, object_id = match.groups()
try: try:
payload = json.loads(payload) payload = json.loads(payload)
@ -88,10 +84,10 @@ def async_start(hass, discovery_topic, hass_config):
_LOGGER.info("Found new component: %s %s", component, discovery_id) _LOGGER.info("Found new component: %s %s", component, discovery_id)
yield from async_load_platform( await async_load_platform(
hass, component, platform, payload, hass_config) hass, component, platform, payload, hass_config)
yield from mqtt.async_subscribe( await mqtt.async_subscribe(
hass, discovery_topic + '/#', async_device_message_received, 0) hass, discovery_topic + '/#', async_device_message_received, 0)
return True return True

View file

@ -203,8 +203,8 @@ def get_config_value(node, value_index, tries=5):
return None return None
@asyncio.coroutine async def async_setup_platform(hass, config, async_add_devices,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): discovery_info=None):
"""Set up the Z-Wave platform (generic part).""" """Set up the Z-Wave platform (generic part)."""
if discovery_info is None or DATA_NETWORK not in hass.data: if discovery_info is None or DATA_NETWORK not in hass.data:
return False return False
@ -504,8 +504,7 @@ def setup(hass, config):
"target node:%s, instance=%s", node_id, group, "target node:%s, instance=%s", node_id, group,
target_node_id, instance) target_node_id, instance)
@asyncio.coroutine async def async_refresh_entity(service):
def async_refresh_entity(service):
"""Refresh values that specific entity depends on.""" """Refresh values that specific entity depends on."""
entity_id = service.data.get(ATTR_ENTITY_ID) entity_id = service.data.get(ATTR_ENTITY_ID)
async_dispatcher_send( async_dispatcher_send(
@ -559,8 +558,7 @@ def setup(hass, config):
network.start() network.start()
hass.bus.fire(const.EVENT_NETWORK_START) hass.bus.fire(const.EVENT_NETWORK_START)
@asyncio.coroutine async def _check_awaked():
def _check_awaked():
"""Wait for Z-wave awaked state (or timeout) and finalize start.""" """Wait for Z-wave awaked state (or timeout) and finalize start."""
_LOGGER.debug( _LOGGER.debug(
"network state: %d %s", network.state, "network state: %d %s", network.state,
@ -585,7 +583,7 @@ def setup(hass, config):
network.state_str) network.state_str)
break break
else: else:
yield from asyncio.sleep(1, loop=hass.loop) await asyncio.sleep(1, loop=hass.loop)
hass.async_add_job(_finalize_start) hass.async_add_job(_finalize_start)
@ -798,11 +796,10 @@ class ZWaveDeviceEntityValues():
dict_id = id(self) dict_id = id(self)
@asyncio.coroutine async def discover_device(component, device, dict_id):
def discover_device(component, device, dict_id):
"""Put device in a dictionary and call discovery on it.""" """Put device in a dictionary and call discovery on it."""
self._hass.data[DATA_DEVICES][dict_id] = device self._hass.data[DATA_DEVICES][dict_id] = device
yield from discovery.async_load_platform( await discovery.async_load_platform(
self._hass, component, DOMAIN, self._hass, component, DOMAIN,
{const.DISCOVERY_DEVICE: dict_id}, self._zwave_config) {const.DISCOVERY_DEVICE: dict_id}, self._zwave_config)
self._hass.add_job(discover_device, component, device, dict_id) self._hass.add_job(discover_device, component, device, dict_id)
@ -844,8 +841,7 @@ class ZWaveDeviceEntity(ZWaveBaseEntity):
self.update_properties() self.update_properties()
self.maybe_schedule_update() self.maybe_schedule_update()
@asyncio.coroutine async def async_added_to_hass(self):
def async_added_to_hass(self):
"""Add device to dict.""" """Add device to dict."""
async_dispatcher_connect( async_dispatcher_connect(
self.hass, self.hass,

View file

@ -79,7 +79,7 @@ def callback(func: Callable[..., None]) -> Callable[..., None]:
def is_callback(func: Callable[..., Any]) -> bool: def is_callback(func: Callable[..., Any]) -> bool:
"""Check if function is safe to be called in the event loop.""" """Check if function is safe to be called in the event loop."""
return '_hass_callback' in func.__dict__ return '_hass_callback' in getattr(func, '__dict__', {})
@callback @callback

View file

@ -5,8 +5,6 @@ There are two different types of discoveries that can be fired/listened for.
- listen_platform/discover_platform is for platforms. These are used by - listen_platform/discover_platform is for platforms. These are used by
components to allow discovery of their platforms. components to allow discovery of their platforms.
""" """
import asyncio
from homeassistant import setup, core from homeassistant import setup, core
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.const import ( from homeassistant.const import (
@ -58,9 +56,8 @@ def discover(hass, service, discovered=None, component=None, hass_config=None):
async_discover(hass, service, discovered, component, hass_config)) async_discover(hass, service, discovered, component, hass_config))
@asyncio.coroutine
@bind_hass @bind_hass
def async_discover(hass, service, discovered=None, component=None, async def async_discover(hass, service, discovered=None, component=None,
hass_config=None): hass_config=None):
"""Fire discovery event. Can ensure a component is loaded.""" """Fire discovery event. Can ensure a component is loaded."""
if component in DEPENDENCY_BLACKLIST: if component in DEPENDENCY_BLACKLIST:
@ -68,7 +65,7 @@ def async_discover(hass, service, discovered=None, component=None,
'Cannot discover the {} component.'.format(component)) 'Cannot discover the {} component.'.format(component))
if component is not None and component not in hass.config.components: if component is not None and component not in hass.config.components:
yield from setup.async_setup_component( await setup.async_setup_component(
hass, component, hass_config) hass, component, hass_config)
data = { data = {
@ -134,9 +131,8 @@ def load_platform(hass, component, platform, discovered=None,
hass_config)) hass_config))
@asyncio.coroutine
@bind_hass @bind_hass
def async_load_platform(hass, component, platform, discovered=None, async def async_load_platform(hass, component, platform, discovered=None,
hass_config=None): hass_config=None):
"""Load a component and platform dynamically. """Load a component and platform dynamically.
@ -148,7 +144,7 @@ def async_load_platform(hass, component, platform, discovered=None,
Use `listen_platform` to register a callback for these events. Use `listen_platform` to register a callback for these events.
Warning: Do not yield from this inside a setup method to avoid a dead lock. Warning: Do not await this inside a setup method to avoid a dead lock.
Use `hass.async_add_job(async_load_platform(..))` instead. Use `hass.async_add_job(async_load_platform(..))` instead.
This method is a coroutine. This method is a coroutine.
@ -160,7 +156,7 @@ def async_load_platform(hass, component, platform, discovered=None,
setup_success = True setup_success = True
if component not in hass.config.components: if component not in hass.config.components:
setup_success = yield from setup.async_setup_component( setup_success = await setup.async_setup_component(
hass, component, hass_config) hass, component, hass_config)
# No need to fire event if we could not setup component # No need to fire event if we could not setup component

View file

@ -261,6 +261,16 @@ class Throttle(object):
def __call__(self, method): def __call__(self, method):
"""Caller for the throttle.""" """Caller for the throttle."""
# Make sure we return a coroutine if the method is async.
if asyncio.iscoroutinefunction(method):
async def throttled_value():
"""Stand-in function for when real func is being throttled."""
return None
else:
def throttled_value():
"""Stand-in function for when real func is being throttled."""
return None
if self.limit_no_throttle is not None: if self.limit_no_throttle is not None:
method = Throttle(self.limit_no_throttle)(method) method = Throttle(self.limit_no_throttle)(method)
@ -277,16 +287,6 @@ class Throttle(object):
is_func = (not hasattr(method, '__self__') and is_func = (not hasattr(method, '__self__') and
'.' not in method.__qualname__.split('.<locals>.')[-1]) '.' not in method.__qualname__.split('.<locals>.')[-1])
# Make sure we return a coroutine if the method is async.
if asyncio.iscoroutinefunction(method):
async def throttled_value():
"""Stand-in function for when real func is being throttled."""
return None
else:
def throttled_value():
"""Stand-in function for when real func is being throttled."""
return None
@wraps(method) @wraps(method)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
"""Wrap that allows wrapped to be called only once per min_time. """Wrap that allows wrapped to be called only once per min_time.

View file

@ -74,7 +74,7 @@ aiodns==1.1.1
aiohttp_cors==0.6.0 aiohttp_cors==0.6.0
# homeassistant.components.hue # homeassistant.components.hue
aiohue==0.3.0 aiohue==1.2.0
# homeassistant.components.sensor.imap # homeassistant.components.sensor.imap
aioimaplib==0.7.13 aioimaplib==0.7.13
@ -568,9 +568,6 @@ pdunehd==1.3
# homeassistant.components.media_player.pandora # homeassistant.components.media_player.pandora
pexpect==4.0.1 pexpect==4.0.1
# homeassistant.components.hue
phue==1.0
# homeassistant.components.rpi_pfio # homeassistant.components.rpi_pfio
pifacecommon==4.1.2 pifacecommon==4.1.2

View file

@ -35,7 +35,7 @@ aioautomatic==0.6.5
aiohttp_cors==0.6.0 aiohttp_cors==0.6.0
# homeassistant.components.hue # homeassistant.components.hue
aiohue==0.3.0 aiohue==1.2.0
# homeassistant.components.notify.apns # homeassistant.components.notify.apns
apns2==0.3.0 apns2==0.3.0

View file

@ -0,0 +1 @@
"""Tests for the Hue component."""

View file

@ -0,0 +1,17 @@
"""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

@ -0,0 +1,98 @@
"""Test Hue bridge."""
import asyncio
from unittest.mock import Mock, patch
import aiohue
import pytest
from homeassistant.components import hue
from tests.common import mock_coro
class MockBridge(hue.HueBridge):
"""Class that sets default for constructor."""
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)
@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):
"""."""
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

View file

@ -0,0 +1,184 @@
"""Tests for Philips Hue config flow."""
import asyncio
from unittest.mock import patch
import aiohue
import pytest
import voluptuous as vol
from homeassistant.components import hue
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=[
{'internalipaddress': '1.2.3.4', 'id': 'bla'}
])
flow = hue.HueFlowHandler()
flow.hass = hass
await flow.async_step_init()
with patch('aiohue.Bridge') as mock_bridge:
def mock_constructor(host, websession):
mock_bridge.host = host
return mock_bridge
mock_bridge.side_effect = mock_constructor
mock_bridge.username = 'username-abc'
mock_bridge.config.name = 'Mock Bridge'
mock_bridge.config.bridgeid = 'bridge-id-1234'
mock_bridge.create_user.return_value = mock_coro()
mock_bridge.initialize.return_value = mock_coro()
result = await flow.async_step_link(user_input={})
assert mock_bridge.host == '1.2.3.4'
assert len(mock_bridge.create_user.mock_calls) == 1
assert len(mock_bridge.initialize.mock_calls) == 1
assert result['type'] == 'create_entry'
assert result['title'] == 'Mock Bridge'
assert result['data'] == {
'host': '1.2.3.4',
'bridge_id': 'bridge-id-1234',
'username': 'username-abc'
}
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()
flow.hass = hass
result = await flow.async_step_init()
assert result['type'] == 'abort'
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=[
{'internalipaddress': '1.2.3.4', 'id': 'bla'}
])
MockConfigEntry(domain='hue', data={
'host': '1.2.3.4'
}).add_to_hass(hass)
flow = hue.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_init()
assert result['type'] == 'abort'
async def test_flow_one_bridge_discovered(hass, aioclient_mock):
"""Test config flow discovers one bridge."""
aioclient_mock.get(hue.API_NUPNP, json=[
{'internalipaddress': '1.2.3.4', 'id': 'bla'}
])
flow = hue.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_init()
assert result['type'] == 'form'
assert result['step_id'] == 'link'
async def test_flow_two_bridges_discovered(hass, aioclient_mock):
"""Test config flow discovers two bridges."""
aioclient_mock.get(hue.API_NUPNP, json=[
{'internalipaddress': '1.2.3.4', 'id': 'bla'},
{'internalipaddress': '5.6.7.8', 'id': 'beer'}
])
flow = hue.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_init()
assert result['type'] == 'form'
assert result['step_id'] == 'init'
with pytest.raises(vol.Invalid):
assert result['data_schema']({'host': '0.0.0.0'})
result['data_schema']({'host': '1.2.3.4'})
result['data_schema']({'host': '5.6.7.8'})
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=[
{'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.hass = hass
result = await flow.async_step_init()
assert result['type'] == 'form'
assert result['step_id'] == 'link'
assert flow.host == '5.6.7.8'
async def test_flow_timeout_discovery(hass):
"""Test config flow ."""
flow = hue.HueFlowHandler()
flow.hass = hass
with patch('aiohue.discovery.discover_nupnp',
side_effect=asyncio.TimeoutError):
result = await flow.async_step_init()
assert result['type'] == 'abort'
async def test_flow_link_timeout(hass):
"""Test config flow ."""
flow = hue.HueFlowHandler()
flow.hass = hass
with patch('aiohue.Bridge.create_user',
side_effect=asyncio.TimeoutError):
result = await flow.async_step_link({})
assert result['type'] == 'form'
assert result['step_id'] == 'link'
assert result['errors'] == {
'base': 'register_failed'
}
async def test_flow_link_button_not_pressed(hass):
"""Test config flow ."""
flow = hue.HueFlowHandler()
flow.hass = hass
with patch('aiohue.Bridge.create_user',
side_effect=aiohue.LinkButtonNotPressed):
result = await flow.async_step_link({})
assert result['type'] == 'form'
assert result['step_id'] == 'link'
assert result['errors'] == {
'base': 'register_failed'
}
async def test_flow_link_unknown_host(hass):
"""Test config flow ."""
flow = hue.HueFlowHandler()
flow.hass = hass
with patch('aiohue.Bridge.create_user',
side_effect=aiohue.RequestError):
result = await flow.async_step_link({})
assert result['type'] == 'form'
assert result['step_id'] == 'link'
assert result['errors'] == {
'base': 'register_failed'
}

View file

@ -0,0 +1,74 @@
"""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']
assert len(hass.data[hue.DOMAIN]) == 2
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'
assert len(hass.data[hue.DOMAIN]) == 1
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'
assert len(hass.data[hue.DOMAIN]) == 1
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
assert len(hass.data[hue.DOMAIN]) == 1
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
assert len(hass.data[hue.DOMAIN]) == 0

File diff suppressed because it is too large Load diff

View file

@ -1,588 +0,0 @@
"""Generic Philips Hue component tests."""
import asyncio
import logging
import unittest
from unittest.mock import call, MagicMock, patch
import aiohue
import pytest
import voluptuous as vol
from homeassistant.components import configurator, hue
from homeassistant.const import CONF_FILENAME, CONF_HOST
from homeassistant.setup import setup_component, async_setup_component
from tests.common import (
assert_setup_component, get_test_home_assistant, get_test_config_dir,
MockDependency, MockConfigEntry, mock_coro
)
_LOGGER = logging.getLogger(__name__)
class TestSetup(unittest.TestCase):
"""Test the Hue component."""
def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.skip_teardown_stop = False
def tearDown(self):
"""Stop everything that was started."""
if not self.skip_teardown_stop:
self.hass.stop()
@MockDependency('phue')
def test_setup_no_domain(self, mock_phue):
"""If it's not in the config we won't even try."""
with assert_setup_component(0):
self.assertTrue(setup_component(
self.hass, hue.DOMAIN, {}))
mock_phue.Bridge.assert_not_called()
self.assertEqual({}, self.hass.data[hue.DOMAIN])
@MockDependency('phue')
def test_setup_with_host(self, mock_phue):
"""Host specified in the config file."""
mock_bridge = mock_phue.Bridge
with assert_setup_component(1):
with patch('homeassistant.helpers.discovery.load_platform') \
as mock_load:
self.assertTrue(setup_component(
self.hass, hue.DOMAIN,
{hue.DOMAIN: {hue.CONF_BRIDGES: [
{CONF_HOST: 'localhost'}]}}))
mock_bridge.assert_called_once_with(
'localhost',
config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE))
mock_load.assert_called_once_with(
self.hass, 'light', hue.DOMAIN,
{'bridge_id': '127.0.0.1'})
self.assertTrue(hue.DOMAIN in self.hass.data)
self.assertEqual(1, len(self.hass.data[hue.DOMAIN]))
@MockDependency('phue')
def test_setup_with_phue_conf(self, mock_phue):
"""No host in the config file, but one is cached in phue.conf."""
mock_bridge = mock_phue.Bridge
with assert_setup_component(1):
with patch(
'homeassistant.components.hue._find_host_from_config',
return_value='localhost'):
with patch('homeassistant.helpers.discovery.load_platform') \
as mock_load:
self.assertTrue(setup_component(
self.hass, hue.DOMAIN,
{hue.DOMAIN: {hue.CONF_BRIDGES: [
{CONF_FILENAME: 'phue.conf'}]}}))
mock_bridge.assert_called_once_with(
'localhost',
config_file_path=get_test_config_dir(
hue.PHUE_CONFIG_FILE))
mock_load.assert_called_once_with(
self.hass, 'light', hue.DOMAIN,
{'bridge_id': '127.0.0.1'})
self.assertTrue(hue.DOMAIN in self.hass.data)
self.assertEqual(1, len(self.hass.data[hue.DOMAIN]))
@MockDependency('phue')
def test_setup_with_multiple_hosts(self, mock_phue):
"""Multiple hosts specified in the config file."""
mock_bridge = mock_phue.Bridge
with assert_setup_component(1):
with patch('homeassistant.helpers.discovery.load_platform') \
as mock_load:
self.assertTrue(setup_component(
self.hass, hue.DOMAIN,
{hue.DOMAIN: {hue.CONF_BRIDGES: [
{CONF_HOST: 'localhost'},
{CONF_HOST: '192.168.0.1'}]}}))
mock_bridge.assert_has_calls([
call(
'localhost',
config_file_path=get_test_config_dir(
hue.PHUE_CONFIG_FILE)),
call(
'192.168.0.1',
config_file_path=get_test_config_dir(
hue.PHUE_CONFIG_FILE))])
mock_load.mock_bridge.assert_not_called()
mock_load.assert_has_calls([
call(
self.hass, 'light', hue.DOMAIN,
{'bridge_id': '127.0.0.1'}),
call(
self.hass, 'light', hue.DOMAIN,
{'bridge_id': '192.168.0.1'}),
], any_order=True)
self.assertTrue(hue.DOMAIN in self.hass.data)
self.assertEqual(2, len(self.hass.data[hue.DOMAIN]))
@MockDependency('phue')
def test_bridge_discovered(self, mock_phue):
"""Bridge discovery."""
mock_bridge = mock_phue.Bridge
mock_service = MagicMock()
discovery_info = {'host': '192.168.0.10', 'serial': 'foobar'}
with patch('homeassistant.helpers.discovery.load_platform') \
as mock_load:
self.assertTrue(setup_component(
self.hass, hue.DOMAIN, {}))
hue.bridge_discovered(self.hass, mock_service, discovery_info)
mock_bridge.assert_called_once_with(
'192.168.0.10',
config_file_path=get_test_config_dir('phue-foobar.conf'))
mock_load.assert_called_once_with(
self.hass, 'light', hue.DOMAIN,
{'bridge_id': '192.168.0.10'})
self.assertTrue(hue.DOMAIN in self.hass.data)
self.assertEqual(1, len(self.hass.data[hue.DOMAIN]))
@MockDependency('phue')
def test_bridge_configure_and_discovered(self, mock_phue):
"""Bridge is in the config file, then we discover it."""
mock_bridge = mock_phue.Bridge
mock_service = MagicMock()
discovery_info = {'host': '192.168.1.10', 'serial': 'foobar'}
with assert_setup_component(1):
with patch('homeassistant.helpers.discovery.load_platform') \
as mock_load:
# First we set up the component from config
self.assertTrue(setup_component(
self.hass, hue.DOMAIN,
{hue.DOMAIN: {hue.CONF_BRIDGES: [
{CONF_HOST: '192.168.1.10'}]}}))
mock_bridge.assert_called_once_with(
'192.168.1.10',
config_file_path=get_test_config_dir(
hue.PHUE_CONFIG_FILE))
calls_to_mock_load = [
call(
self.hass, 'light', hue.DOMAIN,
{'bridge_id': '192.168.1.10'}),
]
mock_load.assert_has_calls(calls_to_mock_load)
self.assertTrue(hue.DOMAIN in self.hass.data)
self.assertEqual(1, len(self.hass.data[hue.DOMAIN]))
# Then we discover the same bridge
hue.bridge_discovered(self.hass, mock_service, discovery_info)
# No additional calls
mock_bridge.assert_called_once_with(
'192.168.1.10',
config_file_path=get_test_config_dir(
hue.PHUE_CONFIG_FILE))
mock_load.assert_has_calls(calls_to_mock_load)
# Still only one
self.assertTrue(hue.DOMAIN in self.hass.data)
self.assertEqual(1, len(self.hass.data[hue.DOMAIN]))
class TestHueBridge(unittest.TestCase):
"""Test the HueBridge class."""
def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.hass.data[hue.DOMAIN] = {}
self.skip_teardown_stop = False
def tearDown(self):
"""Stop everything that was started."""
if not self.skip_teardown_stop:
self.hass.stop()
@MockDependency('phue')
def test_setup_bridge_connection_refused(self, mock_phue):
"""Test a registration failed with a connection refused exception."""
mock_bridge = mock_phue.Bridge
mock_bridge.side_effect = ConnectionRefusedError()
bridge = hue.HueBridge(
'localhost', self.hass, hue.PHUE_CONFIG_FILE, None)
bridge.setup()
self.assertFalse(bridge.configured)
self.assertTrue(bridge.config_request_id is None)
mock_bridge.assert_called_once_with(
'localhost',
config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE))
@MockDependency('phue')
def test_setup_bridge_registration_exception(self, mock_phue):
"""Test a registration failed with an exception."""
mock_bridge = mock_phue.Bridge
mock_phue.PhueRegistrationException = Exception
mock_bridge.side_effect = mock_phue.PhueRegistrationException(1, 2)
bridge = hue.HueBridge(
'localhost', self.hass, hue.PHUE_CONFIG_FILE, None)
bridge.setup()
self.assertFalse(bridge.configured)
self.assertFalse(bridge.config_request_id is None)
self.assertTrue(isinstance(bridge.config_request_id, str))
mock_bridge.assert_called_once_with(
'localhost',
config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE))
@MockDependency('phue')
def test_setup_bridge_registration_succeeds(self, mock_phue):
"""Test a registration success sequence."""
mock_bridge = mock_phue.Bridge
mock_phue.PhueRegistrationException = Exception
mock_bridge.side_effect = [
# First call, raise because not registered
mock_phue.PhueRegistrationException(1, 2),
# Second call, registration is done
None,
]
bridge = hue.HueBridge(
'localhost', self.hass, hue.PHUE_CONFIG_FILE, None)
bridge.setup()
self.assertFalse(bridge.configured)
self.assertFalse(bridge.config_request_id is None)
# Simulate the user confirming the registration
self.hass.services.call(
configurator.DOMAIN, configurator.SERVICE_CONFIGURE,
{configurator.ATTR_CONFIGURE_ID: bridge.config_request_id})
self.hass.block_till_done()
self.assertTrue(bridge.configured)
self.assertTrue(bridge.config_request_id is None)
# We should see a total of two identical calls
args = call(
'localhost',
config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE))
mock_bridge.assert_has_calls([args, args])
# Make sure the request is done
self.assertEqual(1, len(self.hass.states.all()))
self.assertEqual('configured', self.hass.states.all()[0].state)
@MockDependency('phue')
def test_setup_bridge_registration_fails(self, mock_phue):
"""
Test a registration failure sequence.
This may happen when we start the registration process, the user
responds to the request but the bridge has become unreachable.
"""
mock_bridge = mock_phue.Bridge
mock_phue.PhueRegistrationException = Exception
mock_bridge.side_effect = [
# First call, raise because not registered
mock_phue.PhueRegistrationException(1, 2),
# Second call, the bridge has gone away
ConnectionRefusedError(),
]
bridge = hue.HueBridge(
'localhost', self.hass, hue.PHUE_CONFIG_FILE, None)
bridge.setup()
self.assertFalse(bridge.configured)
self.assertFalse(bridge.config_request_id is None)
# Simulate the user confirming the registration
self.hass.services.call(
configurator.DOMAIN, configurator.SERVICE_CONFIGURE,
{configurator.ATTR_CONFIGURE_ID: bridge.config_request_id})
self.hass.block_till_done()
self.assertFalse(bridge.configured)
self.assertFalse(bridge.config_request_id is None)
# We should see a total of two identical calls
args = call(
'localhost',
config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE))
mock_bridge.assert_has_calls([args, args])
# The request should still be pending
self.assertEqual(1, len(self.hass.states.all()))
self.assertEqual('configure', self.hass.states.all()[0].state)
@MockDependency('phue')
def test_setup_bridge_registration_retry(self, mock_phue):
"""
Test a registration retry sequence.
This may happen when we start the registration process, the user
responds to the request but we fail to confirm it with the bridge.
"""
mock_bridge = mock_phue.Bridge
mock_phue.PhueRegistrationException = Exception
mock_bridge.side_effect = [
# First call, raise because not registered
mock_phue.PhueRegistrationException(1, 2),
# Second call, for whatever reason authentication fails
mock_phue.PhueRegistrationException(1, 2),
]
bridge = hue.HueBridge(
'localhost', self.hass, hue.PHUE_CONFIG_FILE, None)
bridge.setup()
self.assertFalse(bridge.configured)
self.assertFalse(bridge.config_request_id is None)
# Simulate the user confirming the registration
self.hass.services.call(
configurator.DOMAIN, configurator.SERVICE_CONFIGURE,
{configurator.ATTR_CONFIGURE_ID: bridge.config_request_id})
self.hass.block_till_done()
self.assertFalse(bridge.configured)
self.assertFalse(bridge.config_request_id is None)
# We should see a total of two identical calls
args = call(
'localhost',
config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE))
mock_bridge.assert_has_calls([args, args])
# Make sure the request is done
self.assertEqual(1, len(self.hass.states.all()))
self.assertEqual('configure', self.hass.states.all()[0].state)
self.assertEqual(
'Failed to register, please try again.',
self.hass.states.all()[0].attributes.get(configurator.ATTR_ERRORS))
@MockDependency('phue')
def test_hue_activate_scene(self, mock_phue):
"""Test the hue_activate_scene service."""
with patch('homeassistant.helpers.discovery.load_platform'):
bridge = hue.HueBridge('localhost', self.hass,
hue.PHUE_CONFIG_FILE, None)
bridge.setup()
# No args
self.hass.services.call(hue.DOMAIN, hue.SERVICE_HUE_SCENE,
blocking=True)
bridge.bridge.run_scene.assert_not_called()
# Only one arg
self.hass.services.call(
hue.DOMAIN, hue.SERVICE_HUE_SCENE,
{hue.ATTR_GROUP_NAME: 'group'},
blocking=True)
bridge.bridge.run_scene.assert_not_called()
self.hass.services.call(
hue.DOMAIN, hue.SERVICE_HUE_SCENE,
{hue.ATTR_SCENE_NAME: 'scene'},
blocking=True)
bridge.bridge.run_scene.assert_not_called()
# Both required args
self.hass.services.call(
hue.DOMAIN, hue.SERVICE_HUE_SCENE,
{hue.ATTR_GROUP_NAME: 'group', hue.ATTR_SCENE_NAME: 'scene'},
blocking=True)
bridge.bridge.run_scene.assert_called_once_with('group', 'scene')
async def test_setup_no_host(hass, requests_mock):
"""No host specified in any way."""
requests_mock.get(hue.API_NUPNP, json=[])
with MockDependency('phue') as mock_phue:
result = await async_setup_component(
hass, hue.DOMAIN, {hue.DOMAIN: {}})
assert result
mock_phue.Bridge.assert_not_called()
assert hass.data[hue.DOMAIN] == {}
async def test_flow_works(hass, aioclient_mock):
"""Test config flow ."""
aioclient_mock.get(hue.API_NUPNP, json=[
{'internalipaddress': '1.2.3.4', 'id': 'bla'}
])
flow = hue.HueFlowHandler()
flow.hass = hass
await flow.async_step_init()
with patch('aiohue.Bridge') as mock_bridge:
def mock_constructor(host, websession):
mock_bridge.host = host
return mock_bridge
mock_bridge.side_effect = mock_constructor
mock_bridge.username = 'username-abc'
mock_bridge.config.name = 'Mock Bridge'
mock_bridge.config.bridgeid = 'bridge-id-1234'
mock_bridge.create_user.return_value = mock_coro()
mock_bridge.initialize.return_value = mock_coro()
result = await flow.async_step_link(user_input={})
assert mock_bridge.host == '1.2.3.4'
assert len(mock_bridge.create_user.mock_calls) == 1
assert len(mock_bridge.initialize.mock_calls) == 1
assert result['type'] == 'create_entry'
assert result['title'] == 'Mock Bridge'
assert result['data'] == {
'host': '1.2.3.4',
'bridge_id': 'bridge-id-1234',
'username': 'username-abc'
}
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()
flow.hass = hass
result = await flow.async_step_init()
assert result['type'] == 'abort'
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=[
{'internalipaddress': '1.2.3.4', 'id': 'bla'}
])
MockConfigEntry(domain='hue', data={
'host': '1.2.3.4'
}).add_to_hass(hass)
flow = hue.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_init()
assert result['type'] == 'abort'
async def test_flow_one_bridge_discovered(hass, aioclient_mock):
"""Test config flow discovers one bridge."""
aioclient_mock.get(hue.API_NUPNP, json=[
{'internalipaddress': '1.2.3.4', 'id': 'bla'}
])
flow = hue.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_init()
assert result['type'] == 'form'
assert result['step_id'] == 'link'
async def test_flow_two_bridges_discovered(hass, aioclient_mock):
"""Test config flow discovers two bridges."""
aioclient_mock.get(hue.API_NUPNP, json=[
{'internalipaddress': '1.2.3.4', 'id': 'bla'},
{'internalipaddress': '5.6.7.8', 'id': 'beer'}
])
flow = hue.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_init()
assert result['type'] == 'form'
assert result['step_id'] == 'init'
with pytest.raises(vol.Invalid):
assert result['data_schema']({'host': '0.0.0.0'})
result['data_schema']({'host': '1.2.3.4'})
result['data_schema']({'host': '5.6.7.8'})
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=[
{'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.hass = hass
result = await flow.async_step_init()
assert result['type'] == 'form'
assert result['step_id'] == 'link'
assert flow.host == '5.6.7.8'
async def test_flow_timeout_discovery(hass):
"""Test config flow ."""
flow = hue.HueFlowHandler()
flow.hass = hass
with patch('aiohue.discovery.discover_nupnp',
side_effect=asyncio.TimeoutError):
result = await flow.async_step_init()
assert result['type'] == 'abort'
async def test_flow_link_timeout(hass):
"""Test config flow ."""
flow = hue.HueFlowHandler()
flow.hass = hass
with patch('aiohue.Bridge.create_user',
side_effect=asyncio.TimeoutError):
result = await flow.async_step_link({})
assert result['type'] == 'form'
assert result['step_id'] == 'link'
assert result['errors'] == {
'base': 'register_failed'
}
async def test_flow_link_button_not_pressed(hass):
"""Test config flow ."""
flow = hue.HueFlowHandler()
flow.hass = hass
with patch('aiohue.Bridge.create_user',
side_effect=aiohue.LinkButtonNotPressed):
result = await flow.async_step_link({})
assert result['type'] == 'form'
assert result['step_id'] == 'link'
assert result['errors'] == {
'base': 'register_failed'
}
async def test_flow_link_unknown_host(hass):
"""Test config flow ."""
flow = hue.HueFlowHandler()
flow.hass = hass
with patch('aiohue.Bridge.create_user',
side_effect=aiohue.RequestError):
result = await flow.async_step_link({})
assert result['type'] == 'form'
assert result['step_id'] == 'link'
assert result['errors'] == {
'base': 'register_failed'
}

View file

@ -17,7 +17,7 @@ from homeassistant.setup import setup_component
import pytest import pytest
from tests.common import ( from tests.common import (
get_test_home_assistant, async_fire_time_changed) get_test_home_assistant, async_fire_time_changed, mock_coro)
from tests.mock.zwave import MockNetwork, MockNode, MockValue, MockEntityValues from tests.mock.zwave import MockNetwork, MockNode, MockValue, MockEntityValues
@ -468,6 +468,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase):
@patch.object(zwave, 'discovery') @patch.object(zwave, 'discovery')
def test_entity_discovery(self, discovery, get_platform): def test_entity_discovery(self, discovery, get_platform):
"""Test the creation of a new entity.""" """Test the creation of a new entity."""
discovery.async_load_platform.return_value = mock_coro()
mock_platform = MagicMock() mock_platform = MagicMock()
get_platform.return_value = mock_platform get_platform.return_value = mock_platform
mock_device = MagicMock() mock_device = MagicMock()
@ -500,8 +501,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase):
key=lambda a: id(a))) key=lambda a: id(a)))
assert discovery.async_load_platform.called assert discovery.async_load_platform.called
# Second call is to async yield from assert len(discovery.async_load_platform.mock_calls) == 1
assert len(discovery.async_load_platform.mock_calls) == 2
args = discovery.async_load_platform.mock_calls[0][1] args = discovery.async_load_platform.mock_calls[0][1]
assert args[0] == self.hass assert args[0] == self.hass
assert args[1] == 'mock_component' assert args[1] == 'mock_component'
@ -532,6 +532,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase):
@patch.object(zwave, 'discovery') @patch.object(zwave, 'discovery')
def test_entity_existing_values(self, discovery, get_platform): def test_entity_existing_values(self, discovery, get_platform):
"""Test the loading of already discovered values.""" """Test the loading of already discovered values."""
discovery.async_load_platform.return_value = mock_coro()
mock_platform = MagicMock() mock_platform = MagicMock()
get_platform.return_value = mock_platform get_platform.return_value = mock_platform
mock_device = MagicMock() mock_device = MagicMock()
@ -563,8 +564,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase):
key=lambda a: id(a))) key=lambda a: id(a)))
assert discovery.async_load_platform.called assert discovery.async_load_platform.called
# Second call is to async yield from assert len(discovery.async_load_platform.mock_calls) == 1
assert len(discovery.async_load_platform.mock_calls) == 2
args = discovery.async_load_platform.mock_calls[0][1] args = discovery.async_load_platform.mock_calls[0][1]
assert args[0] == self.hass assert args[0] == self.hass
assert args[1] == 'mock_component' assert args[1] == 'mock_component'
@ -599,6 +599,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase):
@patch.object(zwave, 'discovery') @patch.object(zwave, 'discovery')
def test_entity_workaround_component(self, discovery, get_platform): def test_entity_workaround_component(self, discovery, get_platform):
"""Test ignore workaround.""" """Test ignore workaround."""
discovery.async_load_platform.return_value = mock_coro()
mock_platform = MagicMock() mock_platform = MagicMock()
get_platform.return_value = mock_platform get_platform.return_value = mock_platform
mock_device = MagicMock() mock_device = MagicMock()
@ -629,8 +630,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase):
self.hass.block_till_done() self.hass.block_till_done()
assert discovery.async_load_platform.called assert discovery.async_load_platform.called
# Second call is to async yield from assert len(discovery.async_load_platform.mock_calls) == 1
assert len(discovery.async_load_platform.mock_calls) == 2
args = discovery.async_load_platform.mock_calls[0][1] args = discovery.async_load_platform.mock_calls[0][1]
assert args[1] == 'binary_sensor' assert args[1] == 'binary_sensor'

View file

@ -1,5 +1,4 @@
"""Test discovery helpers.""" """Test discovery helpers."""
import asyncio
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
@ -24,7 +23,8 @@ class TestHelpersDiscovery:
"""Stop everything that was started.""" """Stop everything that was started."""
self.hass.stop() self.hass.stop()
@patch('homeassistant.setup.async_setup_component') @patch('homeassistant.setup.async_setup_component',
return_value=mock_coro())
def test_listen(self, mock_setup_component): def test_listen(self, mock_setup_component):
"""Test discovery listen/discover combo.""" """Test discovery listen/discover combo."""
helpers = self.hass.helpers helpers = self.hass.helpers
@ -199,15 +199,13 @@ class TestHelpersDiscovery:
assert len(component_calls) == 1 assert len(component_calls) == 1
@asyncio.coroutine async def test_load_platform_forbids_config():
def test_load_platform_forbids_config():
"""Test you cannot setup config component with load_platform.""" """Test you cannot setup config component with load_platform."""
with pytest.raises(HomeAssistantError): with pytest.raises(HomeAssistantError):
yield from discovery.async_load_platform(None, 'config', 'zwave') await discovery.async_load_platform(None, 'config', 'zwave')
@asyncio.coroutine async def test_discover_forbids_config():
def test_discover_forbids_config():
"""Test you cannot setup config component with load_platform.""" """Test you cannot setup config component with load_platform."""
with pytest.raises(HomeAssistantError): with pytest.raises(HomeAssistantError):
yield from discovery.async_discover(None, None, None, 'config') await discovery.async_discover(None, None, None, 'config')

View file

@ -291,3 +291,11 @@ async def test_throttle_async():
assert (await test_method()) is True assert (await test_method()) is True
assert (await test_method()) is None assert (await test_method()) is None
@util.Throttle(timedelta(seconds=2), timedelta(seconds=0.1))
async def test_method2():
"""Only first call should return a value."""
return True
assert (await test_method2()) is True
assert (await test_method2()) is None