Add support for Automatic OAuth2 authentication (#8962)

* Add support for Automatic OAuth2 authentication

* Fix async conversion of configurator

* Rename method for async

* Use hass.components to get configurator component

* Fix typo

* Move session data to hidden directory

* Make configurator callback optional
This commit is contained in:
Adam Mills 2017-08-14 01:37:50 -04:00 committed by Paulus Schoutsen
parent 8fcec03adf
commit 19d1d748d4
25 changed files with 314 additions and 149 deletions

View file

@ -91,7 +91,7 @@ def request_configuration(hass, config, atv, credentials):
hass.async_add_job(configurator.request_done, instance)
instance = configurator.request_config(
hass, 'Apple TV Authentication', configuration_callback,
'Apple TV Authentication', configuration_callback,
description='Please enter PIN code shown on screen.',
submit_caption='Confirm',
fields=[{'id': 'pin', 'name': 'PIN Code', 'type': 'password'}]

View file

@ -110,7 +110,7 @@ def request_configuration(hass, name, host, serialnumber):
title = '{} ({})'.format(name, host)
request_id = configurator.request_config(
hass, title, configuration_callback,
title, configuration_callback,
description='Functionality: ' + str(AXIS_INCLUDE),
entity_picture="/static/images/logo_axis.png",
link_name='Axis platform documentation',

View file

@ -7,19 +7,21 @@ A callback has to be provided to `request_config` which will be called when
the user has submitted configuration information.
"""
import asyncio
import functools as ft
import logging
from homeassistant.core import callback as async_callback
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
ATTR_ENTITY_PICTURE
from homeassistant.loader import bind_hass
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.util.async import run_callback_threadsafe
_LOGGER = logging.getLogger(__name__)
_REQUESTS = {}
_KEY_INSTANCE = 'configurator'
DATA_REQUESTS = 'configurator_requests'
ATTR_CONFIGURE_ID = 'configure_id'
ATTR_DESCRIPTION = 'description'
ATTR_DESCRIPTION_IMAGE = 'description_image'
@ -39,63 +41,89 @@ STATE_CONFIGURED = 'configured'
@bind_hass
def request_config(
hass, name, callback, description=None, description_image=None,
@async_callback
def async_request_config(
hass, name, callback=None, description=None, description_image=None,
submit_caption=None, fields=None, link_name=None, link_url=None,
entity_picture=None):
"""Create a new request for configuration.
Will return an ID to be used for sequent calls.
"""
instance = run_callback_threadsafe(hass.loop,
_async_get_instance,
hass).result()
instance = hass.data.get(_KEY_INSTANCE)
request_id = instance.request_config(
if instance is None:
instance = hass.data[_KEY_INSTANCE] = Configurator(hass)
request_id = instance.async_request_config(
name, callback,
description, description_image, submit_caption,
fields, link_name, link_url, entity_picture)
_REQUESTS[request_id] = instance
if DATA_REQUESTS not in hass.data:
hass.data[DATA_REQUESTS] = {}
hass.data[DATA_REQUESTS][request_id] = instance
return request_id
def notify_errors(request_id, error):
@bind_hass
def request_config(hass, *args, **kwargs):
"""Create a new request for configuration.
Will return an ID to be used for sequent calls.
"""
return run_callback_threadsafe(
hass.loop, ft.partial(async_request_config, hass, *args, **kwargs)
).result()
@bind_hass
@async_callback
def async_notify_errors(hass, request_id, error):
"""Add errors to a config request."""
try:
_REQUESTS[request_id].notify_errors(request_id, error)
hass.data[DATA_REQUESTS][request_id].async_notify_errors(
request_id, error)
except KeyError:
# If request_id does not exist
pass
def request_done(request_id):
@bind_hass
def notify_errors(hass, request_id, error):
"""Add errors to a config request."""
return run_callback_threadsafe(
hass.loop, async_notify_errors, hass, request_id, error
).result()
@bind_hass
@async_callback
def async_request_done(hass, request_id):
"""Mark a configuration request as done."""
try:
_REQUESTS.pop(request_id).request_done(request_id)
hass.data[DATA_REQUESTS].pop(request_id).async_request_done(request_id)
except KeyError:
# If request_id does not exist
pass
@bind_hass
def request_done(hass, request_id):
"""Mark a configuration request as done."""
return run_callback_threadsafe(
hass.loop, async_request_done, hass, request_id
).result()
@asyncio.coroutine
def async_setup(hass, config):
"""Set up the configurator component."""
return True
@async_callback
def _async_get_instance(hass):
"""Get an instance per hass object."""
instance = hass.data.get(_KEY_INSTANCE)
if instance is None:
instance = hass.data[_KEY_INSTANCE] = Configurator(hass)
return instance
class Configurator(object):
"""The class to keep track of current configuration requests."""
@ -105,14 +133,16 @@ class Configurator(object):
self._cur_id = 0
self._requests = {}
hass.services.async_register(
DOMAIN, SERVICE_CONFIGURE, self.handle_service_call)
DOMAIN, SERVICE_CONFIGURE, self.async_handle_service_call)
def request_config(
@async_callback
def async_request_config(
self, name, callback,
description, description_image, submit_caption,
fields, link_name, link_url, entity_picture):
"""Set up a request for configuration."""
entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass)
entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, name, hass=self.hass)
if fields is None:
fields = []
@ -138,11 +168,12 @@ class Configurator(object):
] if value is not None
})
self.hass.states.set(entity_id, STATE_CONFIGURE, data)
self.hass.states.async_set(entity_id, STATE_CONFIGURE, data)
return request_id
def notify_errors(self, request_id, error):
@async_callback
def async_notify_errors(self, request_id, error):
"""Update the state with errors."""
if not self._validate_request_id(request_id):
return
@ -154,9 +185,10 @@ class Configurator(object):
new_data = dict(state.attributes)
new_data[ATTR_ERRORS] = error
self.hass.states.set(entity_id, STATE_CONFIGURE, new_data)
self.hass.states.async_set(entity_id, STATE_CONFIGURE, new_data)
def request_done(self, request_id):
@async_callback
def async_request_done(self, request_id):
"""Remove the configuration request."""
if not self._validate_request_id(request_id):
return
@ -167,15 +199,16 @@ class Configurator(object):
# the result fo the service call (current design limitation).
# Instead, we will set it to configured to give as feedback but delete
# it shortly after so that it is deleted when the client updates.
self.hass.states.set(entity_id, STATE_CONFIGURED)
self.hass.states.async_set(entity_id, STATE_CONFIGURED)
def deferred_remove(event):
"""Remove the request state."""
self.hass.states.remove(entity_id)
self.hass.states.async_remove(entity_id)
self.hass.bus.listen_once(EVENT_TIME_CHANGED, deferred_remove)
self.hass.bus.async_listen_once(EVENT_TIME_CHANGED, deferred_remove)
def handle_service_call(self, call):
@async_callback
def async_handle_service_call(self, call):
"""Handle a configure service call."""
request_id = call.data.get(ATTR_CONFIGURE_ID)
@ -186,8 +219,8 @@ class Configurator(object):
entity_id, fields, callback = self._requests[request_id]
# field validation goes here?
self.hass.async_add_job(callback, call.data.get(ATTR_FIELDS, {}))
if callback:
self.hass.async_add_job(callback, call.data.get(ATTR_FIELDS, {}))
def _generate_unique_id(self):
"""Generate a unique configurator ID."""

View file

@ -6,28 +6,33 @@ https://home-assistant.io/components/device_tracker.automatic/
"""
import asyncio
from datetime import timedelta
import json
import logging
import os
from aiohttp import web
import voluptuous as vol
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, ATTR_ATTRIBUTES, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_MAC,
ATTR_GPS, ATTR_GPS_ACCURACY)
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP,
EVENT_HOMEASSISTANT_START)
CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
REQUIREMENTS = ['aioautomatic==0.4.0']
REQUIREMENTS = ['aioautomatic==0.5.0']
DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__)
CONF_CLIENT_ID = 'client_id'
CONF_SECRET = 'secret'
CONF_DEVICES = 'devices'
CONF_CURRENT_LOCATION = 'current_location'
DEFAULT_TIMEOUT = 5
@ -38,38 +43,76 @@ ATTR_FUEL_LEVEL = 'fuel_level'
EVENT_AUTOMATIC_UPDATE = 'automatic_update'
AUTOMATIC_CONFIG_FILE = '.automatic/session-{}.json'
DATA_CONFIGURING = 'automatic_configurator_clients'
DATA_REFRESH_TOKEN = 'refresh_token'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_SECRET): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Inclusive(CONF_USERNAME, 'auth'): cv.string,
vol.Inclusive(CONF_PASSWORD, 'auth'): cv.string,
vol.Optional(CONF_CURRENT_LOCATION, default=False): cv.boolean,
vol.Optional(CONF_DEVICES, default=None): vol.All(
cv.ensure_list, [cv.string])
})
def _get_refresh_token_from_file(hass, filename):
"""Attempt to load session data from file."""
path = hass.config.path(filename)
if not os.path.isfile(path):
return None
try:
with open(path) as data_file:
data = json.load(data_file)
if data is None:
return None
return data.get(DATA_REFRESH_TOKEN)
except ValueError:
return None
def _write_refresh_token_to_file(hass, filename, refresh_token):
"""Attempt to store session data to file."""
path = hass.config.path(filename)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w+') as data_file:
json.dump({
DATA_REFRESH_TOKEN: refresh_token
}, data_file)
@asyncio.coroutine
def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Validate the configuration and return an Automatic scanner."""
import aioautomatic
hass.http.register_view(AutomaticAuthCallbackView())
scope = FULL_SCOPE if config.get(CONF_CURRENT_LOCATION) else DEFAULT_SCOPE
client = aioautomatic.Client(
client_id=config[CONF_CLIENT_ID],
client_secret=config[CONF_SECRET],
client_session=async_get_clientsession(hass),
request_kwargs={'timeout': DEFAULT_TIMEOUT})
try:
try:
session = yield from client.create_session_from_password(
FULL_SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD])
except aioautomatic.exceptions.ForbiddenError as exc:
if not str(exc).startswith("invalid_scope"):
raise exc
_LOGGER.info("Client not authorized for current_location scope. "
"location:updated events will not be received.")
session = yield from client.create_session_from_password(
DEFAULT_SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD])
filename = AUTOMATIC_CONFIG_FILE.format(config[CONF_CLIENT_ID])
refresh_token = yield from hass.async_add_job(
_get_refresh_token_from_file, hass, filename)
@asyncio.coroutine
def initialize_data(session):
"""Initialize the AutomaticData object from the created session."""
hass.async_add_job(
_write_refresh_token_to_file, hass, filename,
session.refresh_token)
data = AutomaticData(
hass, client, session, config[CONF_DEVICES], async_see)
@ -77,26 +120,105 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
vehicles = yield from session.get_vehicles()
for vehicle in vehicles:
hass.async_add_job(data.load_vehicle(vehicle))
except aioautomatic.exceptions.AutomaticError as err:
_LOGGER.error(str(err))
return False
@callback
def ws_connect(event):
"""Open the websocket connection."""
hass.async_add_job(data.ws_connect())
# Create a task instead of adding a tracking job, since this task will
# run until the websocket connection is closed.
hass.loop.create_task(data.ws_connect())
@callback
def ws_close(event):
"""Close the websocket connection."""
hass.async_add_job(data.ws_close())
if refresh_token is not None:
try:
session = yield from client.create_session_from_refresh_token(
refresh_token)
yield from initialize_data(session)
return True
except aioautomatic.exceptions.BadRequestError as err:
if str(err) == 'err_invalid_refresh_token':
_LOGGER.error("Stored refresh token was invalid.")
yield from hass.async_add_job(
_write_refresh_token_to_file, hass, filename, None)
else:
_LOGGER.error(str(err))
return False
except aioautomatic.exceptions.AutomaticError as err:
_LOGGER.error(str(err))
return False
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, ws_connect)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, ws_close)
if CONF_USERNAME in config:
try:
session = yield from client.create_session_from_password(
scope, config[CONF_USERNAME], config[CONF_PASSWORD])
yield from initialize_data(session)
return True
except aioautomatic.exceptions.AutomaticError as err:
_LOGGER.error(str(err))
return False
configurator = hass.components.configurator
request_id = configurator.async_request_config(
"Automatic", description=(
"Authorization required for Automatic device tracker."),
link_name="Click here to authorize Home Assistant.",
link_url=client.generate_oauth_url(scope),
entity_picture="/static/images/logo_automatic.png",
)
@asyncio.coroutine
def initialize_callback(code, state):
"""Callback after OAuth2 response is returned."""
try:
session = yield from client.create_session_from_oauth_code(
code, state)
yield from initialize_data(session)
configurator.async_request_done(request_id)
except aioautomatic.exceptions.AutomaticError as err:
_LOGGER.error(str(err))
configurator.async_notify_errors(request_id, str(err))
return False
if DATA_CONFIGURING not in hass.data:
hass.data[DATA_CONFIGURING] = {}
hass.data[DATA_CONFIGURING][client.state] = initialize_callback
return True
class AutomaticAuthCallbackView(HomeAssistantView):
"""Handle OAuth finish callback requests."""
requires_auth = False
url = '/api/automatic/callback'
name = 'api:automatic:callback'
@callback
def get(self, request): # pylint: disable=no-self-use
"""Finish OAuth callback request."""
hass = request.app['hass']
params = request.query
response = web.HTTPFound('/states')
if 'state' not in params or 'code' not in params:
if 'error' in params:
_LOGGER.error(
"Error authorizing Automatic: %s", params['error'])
return response
else:
_LOGGER.error(
"Error authorizing Automatic. Invalid response returned.")
return response
if DATA_CONFIGURING not in hass.data or \
params['state'] not in hass.data[DATA_CONFIGURING]:
_LOGGER.error("Automatic configuration request not found.")
return response
code = params['code']
state = params['state']
initialize_callback = hass.data[DATA_CONFIGURING][state]
hass.async_add_job(initialize_callback(code, state))
return response
class AutomaticData(object):
"""A class representing an Automatic cloud service connection."""
@ -115,6 +237,8 @@ class AutomaticData(object):
lambda name, event: self.hass.async_add_job(
self.handle_event(name, event)))
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.ws_close())
@asyncio.coroutine
def handle_event(self, name, event):
"""Coroutine to update state for a realtime event."""

View file

@ -19,7 +19,6 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.util import slugify
import homeassistant.util.dt as dt_util
from homeassistant.util.location import distance
from homeassistant.loader import get_component
_LOGGER = logging.getLogger(__name__)
@ -209,7 +208,7 @@ class Icloud(DeviceScanner):
if self.accountname in _CONFIGURING:
request_id = _CONFIGURING.pop(self.accountname)
configurator = get_component('configurator')
configurator = self.hass.components.configurator
configurator.request_done(request_id)
# Trigger the next step immediately
@ -217,7 +216,7 @@ class Icloud(DeviceScanner):
def icloud_need_trusted_device(self):
"""We need a trusted device."""
configurator = get_component('configurator')
configurator = self.hass.components.configurator
if self.accountname in _CONFIGURING:
return
@ -229,7 +228,7 @@ class Icloud(DeviceScanner):
devicesstring += "{}: {};".format(i, devicename)
_CONFIGURING[self.accountname] = configurator.request_config(
self.hass, 'iCloud {}'.format(self.accountname),
'iCloud {}'.format(self.accountname),
self.icloud_trusted_device_callback,
description=(
'Please choose your trusted device by entering'
@ -259,17 +258,17 @@ class Icloud(DeviceScanner):
if self.accountname in _CONFIGURING:
request_id = _CONFIGURING.pop(self.accountname)
configurator = get_component('configurator')
configurator = self.hass.components.configurator
configurator.request_done(request_id)
def icloud_need_verification_code(self):
"""Return the verification code."""
configurator = get_component('configurator')
configurator = self.hass.components.configurator
if self.accountname in _CONFIGURING:
return
_CONFIGURING[self.accountname] = configurator.request_config(
self.hass, 'iCloud {}'.format(self.accountname),
'iCloud {}'.format(self.accountname),
self.icloud_verification_callback,
description=('Please enter the validation code:'),
entity_picture="/static/images/config_icloud.png",

View file

@ -13,7 +13,6 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.const import CONF_API_KEY
from homeassistant.loader import get_component
from homeassistant.util import Throttle
REQUIREMENTS = ['python-ecobee-api==0.0.7']
@ -41,7 +40,7 @@ CONFIG_SCHEMA = vol.Schema({
def request_configuration(network, hass, config):
"""Request configuration steps from the user."""
configurator = get_component('configurator')
configurator = hass.components.configurator
if 'ecobee' in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING['ecobee'], "Failed to register, please try again.")
@ -56,7 +55,7 @@ def request_configuration(network, hass, config):
setup_ecobee(hass, network, config)
_CONFIGURING['ecobee'] = configurator.request_config(
hass, "Ecobee", ecobee_configuration_callback,
"Ecobee", ecobee_configuration_callback,
description=(
'Please authorize this app at https://www.ecobee.com/consumer'
'portal/index.html with pin code: ' + network.pin),
@ -73,7 +72,7 @@ def setup_ecobee(hass, network, config):
return
if 'ecobee' in _CONFIGURING:
configurator = get_component('configurator')
configurator = hass.components.configurator
configurator.request_done(_CONFIGURING.pop('ecobee'))
hold_temp = config[DOMAIN].get(CONF_HOLD_TEMP)

View file

@ -13,7 +13,6 @@ from homeassistant.components.fan import (
ATTR_SPEED, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED, FanEntity)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.loader import get_component
import homeassistant.util as util
_CONFIGURING = {}
@ -57,7 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
def request_configuration(device_id, insteonhub, model, hass,
add_devices_callback):
"""Request configuration steps from the user."""
configurator = get_component('configurator')
configurator = hass.components.configurator
# We got an error if this method is called while we are configuring
if device_id in _CONFIGURING:
@ -72,7 +71,7 @@ def request_configuration(device_id, insteonhub, model, hass,
add_devices_callback)
_CONFIGURING[device_id] = configurator.request_config(
hass, 'Insteon ' + model + ' addr: ' + device_id,
'Insteon ' + model + ' addr: ' + device_id,
insteon_fan_config_callback,
description=('Enter a name for ' + model + ' Fan addr: ' + device_id),
entity_picture='/static/images/config_insteon.png',
@ -85,7 +84,7 @@ def setup_fan(device_id, name, insteonhub, hass, add_devices_callback):
"""Set up the fan."""
if device_id in _CONFIGURING:
request_id = _CONFIGURING.pop(device_id)
configurator = get_component('configurator')
configurator = hass.components.configurator
configurator.request_done(request_id)
_LOGGER.info("Device configuration done!")

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View file

@ -23,7 +23,6 @@ from homeassistant.components.light import (
SUPPORT_XY_COLOR, Light, PLATFORM_SCHEMA)
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME)
from homeassistant.loader import get_component
from homeassistant.components.emulated_hue import ATTR_EMULATED_HUE
import homeassistant.helpers.config_validation as cv
@ -164,9 +163,7 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable,
# If we came here and configuring this host, mark as done
if host in _CONFIGURING:
request_id = _CONFIGURING.pop(host)
configurator = get_component('configurator')
configurator = hass.components.configurator
configurator.request_done(request_id)
lights = {}
@ -268,7 +265,7 @@ def request_configuration(host, hass, add_devices, filename,
allow_unreachable, allow_in_emulated_hue,
allow_hue_groups):
"""Request configuration steps from the user."""
configurator = get_component('configurator')
configurator = hass.components.configurator
# We got an error if this method is called while we are configuring
if host in _CONFIGURING:
@ -284,7 +281,7 @@ def request_configuration(host, hass, add_devices, filename,
allow_in_emulated_hue, allow_hue_groups)
_CONFIGURING[host] = configurator.request_config(
hass, "Philips Hue", hue_configuration_callback,
"Philips Hue", hue_configuration_callback,
description=("Press the button on the bridge to register Philips Hue "
"with Home Assistant."),
entity_picture="/static/images/logo_philips_hue.png",

View file

@ -11,7 +11,6 @@ from datetime import timedelta
from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
from homeassistant.loader import get_component
import homeassistant.util as util
_CONFIGURING = {}
@ -54,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
def request_configuration(device_id, insteonhub, model, hass,
add_devices_callback):
"""Request configuration steps from the user."""
configurator = get_component('configurator')
configurator = hass.components.configurator
# We got an error if this method is called while we are configuring
if device_id in _CONFIGURING:
@ -69,7 +68,7 @@ def request_configuration(device_id, insteonhub, model, hass,
add_devices_callback)
_CONFIGURING[device_id] = configurator.request_config(
hass, 'Insteon ' + model + ' addr: ' + device_id,
'Insteon ' + model + ' addr: ' + device_id,
insteon_light_config_callback,
description=('Enter a name for ' + model + ' addr: ' + device_id),
entity_picture='/static/images/config_insteon.png',
@ -82,7 +81,7 @@ def setup_light(device_id, name, insteonhub, hass, add_devices_callback):
"""Set up the light."""
if device_id in _CONFIGURING:
request_id = _CONFIGURING.pop(device_id)
configurator = get_component('configurator')
configurator = hass.components.configurator
configurator.request_done(request_id)
_LOGGER.debug("Device configuration done")

View file

@ -11,7 +11,6 @@ import re
import voluptuous as vol
from homeassistant.loader import get_component
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_ON,
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY,
@ -132,7 +131,7 @@ def setup_bravia(config, pin, hass, add_devices):
# If we came here and configuring this host, mark as done
if host in _CONFIGURING:
request_id = _CONFIGURING.pop(host)
configurator = get_component('configurator')
configurator = hass.components.configurator
configurator.request_done(request_id)
_LOGGER.info("Discovery configuration done")
@ -150,7 +149,7 @@ def request_configuration(config, hass, add_devices):
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
configurator = get_component('configurator')
configurator = hass.components.configurator
# We got an error if this method is called while we are configuring
if host in _CONFIGURING:
@ -171,7 +170,7 @@ def request_configuration(config, hass, add_devices):
request_configuration(config, hass, add_devices)
_CONFIGURING[host] = configurator.request_config(
hass, name, bravia_configuration_callback,
name, bravia_configuration_callback,
description='Enter the Pin shown on your Sony Bravia TV.' +
'If no Pin is shown, enter 0000 to let TV show you a Pin.',
description_image="/static/images/smart-tv.png",

View file

@ -18,7 +18,6 @@ from homeassistant.components.media_player import (
MediaPlayerDevice, PLATFORM_SCHEMA)
from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_OFF, CONF_HOST, CONF_PORT, CONF_NAME)
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['websocket-client==0.37.0']
@ -48,7 +47,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def request_configuration(hass, config, url, add_devices_callback):
"""Request configuration steps from the user."""
configurator = get_component('configurator')
configurator = hass.components.configurator
if 'gpmdp' in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING['gpmdp'], "Failed to register, please try again.")
@ -96,7 +95,7 @@ def request_configuration(hass, config, url, add_devices_callback):
break
_CONFIGURING['gpmdp'] = configurator.request_config(
hass, DEFAULT_NAME, gpmdp_configuration_callback,
DEFAULT_NAME, gpmdp_configuration_callback,
description=(
'Enter the pin that is displayed in the '
'Google Play Music Desktop Player.'),
@ -117,7 +116,7 @@ def setup_gpmdp(hass, config, code, add_devices):
return
if 'gpmdp' in _CONFIGURING:
configurator = get_component('configurator')
configurator = hass.components.configurator
configurator.request_done(_CONFIGURING.pop('gpmdp'))
add_devices([GPMDP(name, url, code)], True)

View file

@ -23,7 +23,6 @@ from homeassistant.const import (
DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.loader import get_component
REQUIREMENTS = ['plexapi==2.0.2']
@ -143,7 +142,7 @@ def setup_plexserver(
# If we came here and configuring this host, mark as done
if host in _CONFIGURING:
request_id = _CONFIGURING.pop(host)
configurator = get_component('configurator')
configurator = hass.components.configurator
configurator.request_done(request_id)
_LOGGER.info("Discovery configuration done")
@ -236,7 +235,7 @@ def setup_plexserver(
def request_configuration(host, hass, config, add_devices_callback):
"""Request configuration steps from the user."""
configurator = get_component('configurator')
configurator = hass.components.configurator
# We got an error if this method is called while we are configuring
if host in _CONFIGURING:
configurator.notify_errors(_CONFIGURING[host],
@ -254,7 +253,6 @@ def request_configuration(host, hass, config, add_devices_callback):
)
_CONFIGURING[host] = configurator.request_config(
hass,
'Plex Media Server',
plex_configuration_callback,
description=('Enter the X-Plex-Token'),

View file

@ -10,7 +10,6 @@ from datetime import timedelta
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.loader import get_component
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_VOLUME_SET,
@ -62,9 +61,9 @@ SCAN_INTERVAL = timedelta(seconds=30)
def request_configuration(hass, config, add_devices, oauth):
"""Request Spotify authorization."""
configurator = get_component('configurator')
configurator = hass.components.configurator
hass.data[DOMAIN] = configurator.request_config(
hass, DEFAULT_NAME, lambda _: None,
DEFAULT_NAME, lambda _: None,
link_name=CONFIGURATOR_LINK_NAME,
link_url=oauth.get_authorize_url(),
description=CONFIGURATOR_DESCRIPTION,
@ -88,7 +87,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
request_configuration(hass, config, add_devices, oauth)
return
if hass.data.get(DOMAIN):
configurator = get_component('configurator')
configurator = hass.components.configurator
configurator.request_done(hass.data.get(DOMAIN))
del hass.data[DOMAIN]
player = SpotifyMediaPlayer(oauth, config.get(CONF_NAME, DEFAULT_NAME),

View file

@ -22,7 +22,6 @@ from homeassistant.const import (
CONF_HOST, CONF_MAC, CONF_CUSTOMIZE, STATE_OFF,
STATE_PLAYING, STATE_PAUSED,
STATE_UNKNOWN, CONF_NAME, CONF_FILENAME)
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pylgtv==0.1.7',
@ -114,7 +113,7 @@ def setup_tv(host, mac, name, customize, config, hass, add_devices):
# If we came here and configuring this host, mark as done.
if client.is_registered() and host in _CONFIGURING:
request_id = _CONFIGURING.pop(host)
configurator = get_component('configurator')
configurator = hass.components.configurator
configurator.request_done(request_id)
add_devices([LgWebOSDevice(host, mac, name, customize, config)], True)
@ -123,7 +122,7 @@ def setup_tv(host, mac, name, customize, config, hass, add_devices):
def request_configuration(
host, mac, name, customize, config, hass, add_devices):
"""Request configuration steps from the user."""
configurator = get_component('configurator')
configurator = hass.components.configurator
# We got an error if this method is called while we are configuring
if host in _CONFIGURING:
@ -137,7 +136,7 @@ def request_configuration(
setup_tv(host, mac, name, customize, config, hass, add_devices)
_CONFIGURING[host] = configurator.request_config(
hass, name, lgtv_configuration_callback,
name, lgtv_configuration_callback,
description='Click start and accept the pairing request on your TV.',
description_image='/static/images/config_webos.png',
submit_caption='Start pairing request'

View file

@ -14,7 +14,6 @@ from homeassistant.helpers import discovery
from homeassistant.const import (
CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS,
CONF_MONITORED_CONDITIONS)
from homeassistant.loader import get_component
REQUIREMENTS = ['python-nest==3.1.0']
@ -54,7 +53,7 @@ CONFIG_SCHEMA = vol.Schema({
def request_configuration(nest, hass, config):
"""Request configuration steps from the user."""
configurator = get_component('configurator')
configurator = hass.components.configurator
if 'nest' in _CONFIGURING:
_LOGGER.debug("configurator failed")
configurator.notify_errors(
@ -68,7 +67,7 @@ def request_configuration(nest, hass, config):
setup_nest(hass, nest, config, pin=pin)
_CONFIGURING['nest'] = configurator.request_config(
hass, "Nest", nest_configuration_callback,
"Nest", nest_configuration_callback,
description=('To configure Nest, click Request Authorization below, '
'log into your Nest account, '
'and then enter the resulting PIN'),
@ -92,7 +91,7 @@ def setup_nest(hass, nest, config, pin=None):
if 'nest' in _CONFIGURING:
_LOGGER.debug("configuration done")
configurator = get_component('configurator')
configurator = hass.components.configurator
configurator.request_done(_CONFIGURING.pop('nest'))
_LOGGER.debug("proceeding with setup")

View file

@ -17,7 +17,6 @@ from homeassistant.components.http import HomeAssistantView
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['fitbit==0.2.3']
@ -155,7 +154,7 @@ def config_from_file(filename, config=None):
def request_app_setup(hass, config, add_devices, config_path,
discovery_info=None):
"""Assist user with configuring the Fitbit dev application."""
configurator = get_component('configurator')
configurator = hass.components.configurator
# pylint: disable=unused-argument
def fitbit_configuration_callback(callback_data):
@ -166,7 +165,8 @@ def request_app_setup(hass, config, add_devices, config_path,
if config_file == DEFAULT_CONFIG:
error_msg = ("You didn't correctly modify fitbit.conf",
" please try again")
configurator.notify_errors(_CONFIGURING['fitbit'], error_msg)
configurator.notify_errors(_CONFIGURING['fitbit'],
error_msg)
else:
setup_platform(hass, config, add_devices, discovery_info)
else:
@ -187,7 +187,7 @@ def request_app_setup(hass, config, add_devices, config_path,
submit = "I have saved my Client ID and Client Secret into fitbit.conf."
_CONFIGURING['fitbit'] = configurator.request_config(
hass, 'Fitbit', fitbit_configuration_callback,
'Fitbit', fitbit_configuration_callback,
description=description, submit_caption=submit,
description_image="/static/images/config_fitbit_app.png"
)
@ -195,7 +195,7 @@ def request_app_setup(hass, config, add_devices, config_path,
def request_oauth_completion(hass):
"""Request user complete Fitbit OAuth2 flow."""
configurator = get_component('configurator')
configurator = hass.components.configurator
if "fitbit" in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING['fitbit'], "Failed to register, please try again.")
@ -211,7 +211,7 @@ def request_oauth_completion(hass):
description = "Please authorize Fitbit by visiting {}".format(start_url)
_CONFIGURING['fitbit'] = configurator.request_config(
hass, 'Fitbit', fitbit_configuration_callback,
'Fitbit', fitbit_configuration_callback,
description=description,
submit_caption="I have authorized Fitbit."
)
@ -233,7 +233,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False
if "fitbit" in _CONFIGURING:
get_component('configurator').request_done(_CONFIGURING.pop("fitbit"))
hass.components.configurator.request_done(_CONFIGURING.pop("fitbit"))
import fitbit

View file

@ -18,7 +18,6 @@ from homeassistant.const import (
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
from homeassistant.loader import get_component
REQUIREMENTS = ['https://github.com/jamespcole/home-assistant-nzb-clients/'
'archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip'
@ -88,7 +87,7 @@ def setup_sabnzbd(base_url, apikey, name, hass, config, add_devices, sab_api):
def request_configuration(host, name, hass, config, add_devices, sab_api):
"""Request configuration steps from the user."""
configurator = get_component('configurator')
configurator = hass.components.configurator
# We got an error if this method is called while we are configuring
if host in _CONFIGURING:
configurator.notify_errors(_CONFIGURING[host],
@ -114,7 +113,6 @@ def request_configuration(host, name, hass, config, add_devices, sab_api):
hass.async_add_job(success)
_CONFIGURING[host] = configurator.request_config(
hass,
DEFAULT_NAME,
sabnzbd_configuration_callback,
description=('Enter the API Key'),

View file

@ -10,7 +10,6 @@ import os
from datetime import timedelta
from homeassistant.components.switch import SwitchDevice
from homeassistant.loader import get_component
import homeassistant.util as util
_CONFIGURING = {}
@ -51,7 +50,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
def request_configuration(
device_id, insteonhub, model, hass, add_devices_callback):
"""Request configuration steps from the user."""
configurator = get_component('configurator')
configurator = hass.components.configurator
# We got an error if this method is called while we are configuring
if device_id in _CONFIGURING:
@ -66,7 +65,7 @@ def request_configuration(
add_devices_callback)
_CONFIGURING[device_id] = configurator.request_config(
hass, 'Insteon Switch ' + model + ' addr: ' + device_id,
'Insteon Switch ' + model + ' addr: ' + device_id,
insteon_switch_config_callback,
description=('Enter a name for ' + model + ' addr: ' + device_id),
entity_picture='/static/images/config_insteon.png',
@ -79,7 +78,7 @@ def setup_switch(device_id, name, insteonhub, hass, add_devices_callback):
"""Set up the switch."""
if device_id in _CONFIGURING:
request_id = _CONFIGURING.pop(device_id)
configurator = get_component('configurator')
configurator = hass.components.configurator
configurator.request_done(request_id)
_LOGGER.info("Device configuration done")

View file

@ -14,7 +14,6 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.const import CONF_HOST, CONF_API_KEY
from homeassistant.loader import get_component
from homeassistant.components.discovery import SERVICE_IKEA_TRADFRI
REQUIREMENTS = ['pytradfri==1.1']
@ -41,7 +40,7 @@ _LOGGER = logging.getLogger(__name__)
def request_configuration(hass, config, host):
"""Request configuration steps from the user."""
configurator = get_component('configurator')
configurator = hass.components.configurator
hass.data.setdefault(KEY_CONFIG, {})
instance = hass.data[KEY_CONFIG].get(host)
@ -70,7 +69,7 @@ def request_configuration(hass, config, host):
hass.async_add_job(success)
instance = configurator.request_config(
hass, "IKEA Trådfri", configuration_callback,
"IKEA Trådfri", configuration_callback,
description='Please enter the security code written at the bottom of '
'your IKEA Trådfri Gateway.',
submit_caption="Confirm",

View file

@ -13,7 +13,6 @@ from datetime import timedelta
import voluptuous as vol
import requests
from homeassistant.loader import get_component
from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView
from homeassistant.helpers import discovery
@ -103,7 +102,7 @@ def _read_config_file(file_path):
def _request_app_setup(hass, config):
"""Assist user with configuring the Wink dev application."""
hass.data[DOMAIN]['configurator'] = True
configurator = get_component('configurator')
configurator = hass.components.configurator
# pylint: disable=unused-argument
def wink_configuration_callback(callback_data):
@ -138,7 +137,7 @@ def _request_app_setup(hass, config):
""".format(start_url)
hass.data[DOMAIN]['configuring'][DOMAIN] = configurator.request_config(
hass, DOMAIN, wink_configuration_callback,
DOMAIN, wink_configuration_callback,
description=description, submit_caption="submit",
description_image="/static/images/config_wink.png",
fields=[{'id': 'client_id', 'name': 'Client ID', 'type': 'string'},
@ -151,7 +150,7 @@ def _request_app_setup(hass, config):
def _request_oauth_completion(hass, config):
"""Request user complete Wink OAuth2 flow."""
hass.data[DOMAIN]['configurator'] = True
configurator = get_component('configurator')
configurator = hass.components.configurator
if DOMAIN in hass.data[DOMAIN]['configuring']:
configurator.notify_errors(
hass.data[DOMAIN]['configuring'][DOMAIN],
@ -168,7 +167,7 @@ def _request_oauth_completion(hass, config):
description = "Please authorize Wink by visiting {}".format(start_url)
hass.data[DOMAIN]['configuring'][DOMAIN] = configurator.request_config(
hass, DOMAIN, wink_configuration_callback,
DOMAIN, wink_configuration_callback,
description=description
)
@ -248,7 +247,7 @@ def setup(hass, config):
if DOMAIN in hass.data[DOMAIN]['configuring']:
_configurator = hass.data[DOMAIN]['configuring']
get_component('configurator').request_done(_configurator.pop(
hass.components.configurator.request_done(_configurator.pop(
DOMAIN))
# Using oauth

View file

@ -39,7 +39,7 @@ SoCo==0.12
TwitterAPI==2.4.6
# homeassistant.components.device_tracker.automatic
aioautomatic==0.4.0
aioautomatic==0.5.0
# homeassistant.components.sensor.dnsip
aiodns==1.1.1

View file

@ -27,7 +27,7 @@ PyJWT==1.5.2
SoCo==0.12
# homeassistant.components.device_tracker.automatic
aioautomatic==0.4.0
aioautomatic==0.5.0
# homeassistant.components.emulated_hue
# homeassistant.components.http

View file

@ -7,12 +7,16 @@ import aioautomatic
from homeassistant.components.device_tracker.automatic import (
async_setup_scanner)
from tests.common import mock_http_component
_LOGGER = logging.getLogger(__name__)
@patch('aioautomatic.Client.create_session_from_password')
def test_invalid_credentials(mock_create_session, hass):
"""Test with invalid credentials."""
mock_http_component(hass)
@asyncio.coroutine
def get_session(*args, **kwargs):
"""Return the test session."""
@ -34,8 +38,15 @@ def test_invalid_credentials(mock_create_session, hass):
@patch('aioautomatic.Client.create_session_from_password')
def test_valid_credentials(mock_create_session, hass):
@patch('aioautomatic.Client.ws_connect')
@patch('json.dump')
@patch('os.makedirs')
@patch('homeassistant.components.device_tracker.automatic.open', create=True)
def test_valid_credentials(mock_open, mock_os_makedirs, mock_json_dump,
mock_ws_connect, mock_create_session, hass):
"""Test with valid credentials."""
mock_http_component(hass)
session = MagicMock()
vehicle = MagicMock()
trip = MagicMock()
@ -66,13 +77,21 @@ def test_valid_credentials(mock_create_session, hass):
return [trip]
mock_create_session.side_effect = get_session
session.ws_connect = MagicMock()
session.get_vehicles.side_effect = get_vehicles
session.get_trips.side_effect = get_trips
session.refresh_token = 'mock_refresh_token'
@asyncio.coroutine
def ws_connect():
return asyncio.Future(loop=hass.loop)
mock_ws_connect.side_effect = ws_connect
config = {
'platform': 'automatic',
'username': 'bad_username',
'password': 'bad_password',
'username': 'good_username',
'password': 'good_password',
'client_id': 'client_id',
'secret': 'client_secret',
'devices': None,
@ -80,6 +99,8 @@ def test_valid_credentials(mock_create_session, hass):
result = hass.loop.run_until_complete(
async_setup_scanner(hass, config, mock_see))
hass.async_block_till_done()
assert result
assert mock_see.called
assert len(mock_see.mock_calls) == 2
@ -89,3 +110,9 @@ def test_valid_credentials(mock_create_session, hass):
assert mock_see.mock_calls[0][2]['attributes'] == {'fuel_level': 45.6}
assert mock_see.mock_calls[0][2]['gps'] == (45.567, 34.345)
assert mock_see.mock_calls[0][2]['gps_accuracy'] == 5.6
assert mock_json_dump.called
assert len(mock_json_dump.mock_calls) == 1
assert mock_json_dump.mock_calls[0][1][0] == {
'refresh_token': 'mock_refresh_token'
}

View file

@ -90,20 +90,20 @@ class TestConfigurator(unittest.TestCase):
request_id = configurator.request_config(
self.hass, "Test Request", lambda _: None)
error = "Oh no bad bad bad"
configurator.notify_errors(request_id, error)
configurator.notify_errors(self.hass, request_id, error)
state = self.hass.states.all()[0]
self.assertEqual(error, state.attributes.get(configurator.ATTR_ERRORS))
def test_notify_errors_fail_silently_on_bad_request_id(self):
"""Test if notify errors fails silently with a bad request id."""
configurator.notify_errors(2015, "Try this error")
configurator.notify_errors(self.hass, 2015, "Try this error")
def test_request_done_works(self):
"""Test if calling request done works."""
request_id = configurator.request_config(
self.hass, "Test Request", lambda _: None)
configurator.request_done(request_id)
configurator.request_done(self.hass, request_id)
self.assertEqual(1, len(self.hass.states.all()))
self.hass.bus.fire(EVENT_TIME_CHANGED)
@ -112,4 +112,4 @@ class TestConfigurator(unittest.TestCase):
def test_request_done_fail_silently_on_bad_request_id(self):
"""Test that request_done fails silently with a bad request id."""
configurator.request_done(2016)
configurator.request_done(self.hass, 2016)