diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 8c1a9751c..62d5b64ff 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -106,6 +106,34 @@ def async_setup(hass, config): cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, cloud.async_start) yield from http_api.async_setup(hass) + + + # TEMP, see iot.py for real TODO + async def async_generate_url(flow_id): + # TODO extract this into helper + websession = hass.helpers.aiohttp_client.async_get_clientsession() + + with async_timeout.timeout(10, loop=hass.loop): + await hass.async_add_job(auth_api.check_token, cloud) + + with async_timeout.timeout(10, loop=hass.loop): + req = await websession.post( + 'https://wijyl1dxe5.execute-api.us-east-1.amazonaws.com' + '/prod/{service}/generate_authorize_url', + headers={ + 'authorization': cloud.id_token + }, + data={ + 'config_flow_id': flow_id, + } + ) + + data = await req.json() + return data['url'] + + hass.components.nest.async_register_flow_handler('Home Assistant Cloud', async_generate_url, False) + + return True diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index 12b81c900..b79125bff 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -5,6 +5,7 @@ import pprint from aiohttp import hdrs, client_exceptions, WSMsgType +from homeassistant import data_entry_flow from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.components.alexa import smart_home as alexa from homeassistant.components.google_assistant import smart_home as ga @@ -48,6 +49,9 @@ class CloudIoT: if self.state != STATE_DISCONNECTED: raise RuntimeError('Connect called while not disconnected') + # TODO add easy way to listen to cloud connect/disconnect events + # That's how we'll subscribe to auth + hass = self.cloud.hass self.close_requested = False self.state = STATE_CONNECTING @@ -255,3 +259,20 @@ def async_handle_cloud(hass, cloud, payload): _LOGGER.warning("Received unknown cloud action: %s", action) return None + + +@HANDLERS.register('account_link') +async def async_handle_account_link(hass, cloud, payload): + """Handle an incoming IoT message for linking accounts.""" + try: + await hass.config_entries.flow.async_configure( + payload['config_flow_id'], payload['tokens']) + except data_entry_flow.UnknownFlow: + return { + 'success': False, + 'reason': 'unknown_config_flow', + } + + return { + 'success': True + } diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 16a0b80d1..a66d06103 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -6,10 +6,14 @@ https://home-assistant.io/components/nest/ """ from concurrent.futures import ThreadPoolExecutor import logging +import uuid import socket import voluptuous as vol +from homeassistant import config_entries, data_entry_flow +from homeassistant.core import callback +from homeassistant.loader import bind_hass from homeassistant.const import ( CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, CONF_MONITORED_CONDITIONS, @@ -27,6 +31,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'nest' DATA_NEST = 'nest' +DATA_NEST_FLOW = 'nest_config_flow' SIGNAL_NEST_UPDATE = 'nest_update' @@ -187,8 +192,9 @@ async def async_setup(hass, config): """Set up Nest components.""" from nest import Nest - if 'nest' in _CONFIGURING: - return + # When we are doing config entries. + if DOMAIN not in config: + return True conf = config[DOMAIN] client_id = conf[CONF_CLIENT_ID] @@ -325,3 +331,67 @@ class NestSensorDevice(Entity): async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state) + + +@bind_hass +def async_register_flow_handler(hass, name, generate_oauth_auth_url, use_pin): + """Register an auth flow for the config entry flow.""" + flows = hass.data.get(DATA_NEST_FLOW) + if flows is None: + flows = hass.data[DATA_NEST_FLOW] = {} + handler_id = uuid.uuid4().hex + data = { + 'name': name, + 'generate_oauth_auth_url': generate_oauth_auth_url, + 'use_pin': use_pin, + } + flows[handler_id] = data + + @callback + def async_unregister_handler(): + """Removes the handler.""" + flows.pop(handler_id, None) + + return async_unregister_handler + + +@config_entries.HANDLERS.register(DOMAIN) +class NestFlowHandler(data_entry_flow.FlowHandler): + """Handle a Nest config flow.""" + + VERSION = 1 + + def __init__(self): + """Initialize Nest config flow.""" + self._flow_handler = None + + async def async_step_init(self, user_input=None): + """Handle a flow start.""" + flows = self.hass.data.get(DATA_NEST_FLOW, []) + + if not flows: + return self.async_abort( + reason='no_flow' + ) + elif len(flows) == 1: + self._flow_handler = list(flows.keys())[0] + return await self.async_step_flow() + + # Handle if we have multiple flows we show a picker. + + async def async_step_flow(self, user_input=None): + """Handle the flow authentication.""" + handler = self.hass.data[DATA_NEST_FLOW][self._flow_handler] + + if user_input: + return self.async_create_entry( + title='Nest temp', + data=user_input, + ) + + url = await handler['generate_oauth_auth_url'](self.flow_id) + + return self.async_external_step( + step_id='flow', + url=url, + ) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 8a73e424f..7826e26b9 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -129,6 +129,7 @@ HANDLERS = Registry() FLOWS = [ 'deconz', 'hue', + 'nest', 'zone', ] diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 5095297e7..fadb62e36 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -13,6 +13,10 @@ SOURCE_DISCOVERY = 'discovery' RESULT_TYPE_FORM = 'form' RESULT_TYPE_CREATE_ENTRY = 'create_entry' RESULT_TYPE_ABORT = 'abort' +RESULT_TYPE_EXTERNAL_STEP = 'external' + +# Event that is fired when a flow is progressed via external source. +EVENT_DATA_ENTRY_FLOW_PROGRESSED = 'data_entry_flow_progressed' class FlowError(HomeAssistantError): @@ -73,13 +77,33 @@ class FlowManager: if flow is None: raise UnknownFlow - step_id, data_schema = flow.cur_step + cur_step = flow.cur_step - if data_schema is not None and user_input is not None: - user_input = data_schema(user_input) + if cur_step.get('data_schema') is not None and user_input is not None: + user_input = cur_step['data_schema'](user_input) - return await self._async_handle_step( - flow, step_id, user_input) + result = await self._async_handle_step( + flow, cur_step['step_id'], user_input) + + # If we just got data from an external step which caused us to make + # progress, fire an event to update the frontend. + if (cur_step['type'] == RESULT_TYPE_EXTERNAL_STEP and + cur_step['step_id'] != result.get('step_id')): + # These results will end the flow, making a refresh impossible. + # So we embed the results. + if result['type'] in (RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY): + event_data = result + else: + # Tell frontend to reload the flow state. + event_data = { + 'handler': flow.handler, + 'flow_id': flow_id, + 'refresh': True + } + self.hass.bus.async_fire(EVENT_DATA_ENTRY_FLOW_PROGRESSED, + event_data) + + return result @callback def async_abort(self, flow_id): @@ -98,13 +122,13 @@ class FlowManager: result = await getattr(flow, method)(user_input) - if result['type'] not in (RESULT_TYPE_FORM, RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_ABORT): + if result['type'] not in (RESULT_TYPE_FORM, RESULT_TYPE_EXTERNAL_STEP, + RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_ABORT): raise ValueError( 'Handler returned incorrect type: {}'.format(result['type'])) - if result['type'] == RESULT_TYPE_FORM: - flow.cur_step = (result['step_id'], result['data_schema']) + if result['type'] in (RESULT_TYPE_FORM, RESULT_TYPE_EXTERNAL_STEP): + flow.cur_step = result return result # Abort and Success results both finish the flow @@ -165,3 +189,14 @@ class FlowHandler: 'handler': self.handler, 'reason': reason } + + @callback + def async_external_step(self, *, step_id, url): + """Return the definition of an external step for the user to take.""" + return { + 'type': RESULT_TYPE_EXTERNAL_STEP, + 'flow_id': self.flow_id, + 'handler': self.handler, + 'step_id': step_id, + 'url': url, + }