"""Config flow to configure Nest.""" import asyncio from collections import OrderedDict import logging import os import async_timeout import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.util.json import load_json from .const import DOMAIN DATA_FLOW_IMPL = "nest_flow_implementation" _LOGGER = logging.getLogger(__name__) @callback def register_flow_implementation(hass, domain, name, gen_authorize_url, convert_code): """Register a flow implementation. domain: Domain of the component responsible for the implementation. name: Name of the component. gen_authorize_url: Coroutine function to generate the authorize url. convert_code: Coroutine function to convert a code to an access token. """ if DATA_FLOW_IMPL not in hass.data: hass.data[DATA_FLOW_IMPL] = OrderedDict() hass.data[DATA_FLOW_IMPL][domain] = { "domain": domain, "name": name, "gen_authorize_url": gen_authorize_url, "convert_code": convert_code, } class NestAuthError(HomeAssistantError): """Base class for Nest auth errors.""" class CodeInvalid(NestAuthError): """Raised when invalid authorization code.""" @config_entries.HANDLERS.register(DOMAIN) class NestFlowHandler(config_entries.ConfigFlow): """Handle a Nest config flow.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH def __init__(self): """Initialize the Nest config flow.""" self.flow_impl = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" return await self.async_step_init(user_input) async def async_step_init(self, user_input=None): """Handle a flow start.""" flows = self.hass.data.get(DATA_FLOW_IMPL, {}) if self.hass.config_entries.async_entries(DOMAIN): return self.async_abort(reason="already_setup") if not flows: return self.async_abort(reason="no_flows") if len(flows) == 1: self.flow_impl = list(flows)[0] return await self.async_step_link() if user_input is not None: self.flow_impl = user_input["flow_impl"] return await self.async_step_link() return self.async_show_form( step_id="init", data_schema=vol.Schema({vol.Required("flow_impl"): vol.In(list(flows))}), ) async def async_step_link(self, user_input=None): """Attempt to link with the Nest account. Route the user to a website to authenticate with Nest. Depending on implementation type we expect a pin or an external component to deliver the authentication code. """ flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl] errors = {} if user_input is not None: try: with async_timeout.timeout(10): tokens = await flow["convert_code"](user_input["code"]) return self._entry_from_tokens( "Nest (via {})".format(flow["name"]), flow, tokens ) except asyncio.TimeoutError: errors["code"] = "timeout" except CodeInvalid: errors["code"] = "invalid_code" except NestAuthError: errors["code"] = "unknown" except Exception: # pylint: disable=broad-except errors["code"] = "internal_error" _LOGGER.exception("Unexpected error resolving code") try: with async_timeout.timeout(10): url = await flow["gen_authorize_url"](self.flow_id) except asyncio.TimeoutError: return self.async_abort(reason="authorize_url_timeout") except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected error generating auth url") return self.async_abort(reason="authorize_url_fail") return self.async_show_form( step_id="link", description_placeholders={"url": url}, data_schema=vol.Schema({vol.Required("code"): str}), errors=errors, ) async def async_step_import(self, info): """Import existing auth from Nest.""" if self.hass.config_entries.async_entries(DOMAIN): return self.async_abort(reason="already_setup") config_path = info["nest_conf_path"] if not await self.hass.async_add_job(os.path.isfile, config_path): self.flow_impl = DOMAIN return await self.async_step_link() flow = self.hass.data[DATA_FLOW_IMPL][DOMAIN] tokens = await self.hass.async_add_job(load_json, config_path) return self._entry_from_tokens( "Nest (import from configuration.yaml)", flow, tokens ) @callback def _entry_from_tokens(self, title, flow, tokens): """Create an entry from tokens.""" return self.async_create_entry( title=title, data={"tokens": tokens, "impl_domain": flow["domain"]} )