core/homeassistant/components/smartthings/config_flow.py

201 lines
7.4 KiB
Python

"""Config flow to configure SmartThings."""
import logging
from aiohttp import ClientResponseError
from pysmartthings import APIResponseError, AppOAuth, SmartThings
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
APP_OAUTH_CLIENT_NAME,
APP_OAUTH_SCOPES,
CONF_APP_ID,
CONF_INSTALLED_APPS,
CONF_LOCATION_ID,
CONF_OAUTH_CLIENT_ID,
CONF_OAUTH_CLIENT_SECRET,
DOMAIN,
VAL_UID_MATCHER,
)
from .smartapp import (
create_app,
find_app,
setup_smartapp,
setup_smartapp_endpoint,
update_app,
validate_webhook_requirements,
)
_LOGGER = logging.getLogger(__name__)
@config_entries.HANDLERS.register(DOMAIN)
class SmartThingsFlowHandler(config_entries.ConfigFlow):
"""
Handle configuration of SmartThings integrations.
Any number of integrations are supported. The high level flow follows:
1) Flow initiated
a) User initiates through the UI
b) Re-configuration of a failed entry setup
2) Enter access token
a) Check not already setup
b) Validate format
c) Setup SmartApp
3) Wait for Installation
a) Check user installed into one or more locations
b) Config entries setup for all installations
"""
VERSION = 2
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
def __init__(self):
"""Create a new instance of the flow handler."""
self.access_token = None
self.app_id = None
self.api = None
self.oauth_client_secret = None
self.oauth_client_id = None
async def async_step_import(self, user_input=None):
"""Occurs when a previously entry setup fails and is re-initiated."""
return await self.async_step_user(user_input)
async def async_step_user(self, user_input=None):
"""Get access token and validate it."""
errors = {}
if user_input is None or CONF_ACCESS_TOKEN not in user_input:
return self._show_step_user(errors)
self.access_token = user_input.get(CONF_ACCESS_TOKEN, "")
self.api = SmartThings(async_get_clientsession(self.hass), self.access_token)
# Ensure token is a UUID
if not VAL_UID_MATCHER.match(self.access_token):
errors[CONF_ACCESS_TOKEN] = "token_invalid_format"
return self._show_step_user(errors)
# Check not already setup in another entry
if any(
entry.data.get(CONF_ACCESS_TOKEN) == self.access_token
for entry in self.hass.config_entries.async_entries(DOMAIN)
):
errors[CONF_ACCESS_TOKEN] = "token_already_setup"
return self._show_step_user(errors)
# Setup end-point
await setup_smartapp_endpoint(self.hass)
if not validate_webhook_requirements(self.hass):
errors["base"] = "base_url_not_https"
return self._show_step_user(errors)
try:
app = await find_app(self.hass, self.api)
if app:
await app.refresh() # load all attributes
await update_app(self.hass, app)
# Get oauth client id/secret by regenerating it
app_oauth = AppOAuth(app.app_id)
app_oauth.client_name = APP_OAUTH_CLIENT_NAME
app_oauth.scope.extend(APP_OAUTH_SCOPES)
client = await self.api.generate_app_oauth(app_oauth)
else:
app, client = await create_app(self.hass, self.api)
setup_smartapp(self.hass, app)
self.app_id = app.app_id
self.oauth_client_secret = client.client_secret
self.oauth_client_id = client.client_id
except APIResponseError as ex:
if ex.is_target_error():
errors["base"] = "webhook_error"
else:
errors["base"] = "app_setup_error"
_LOGGER.exception(
"API error setting up the SmartApp: %s", ex.raw_error_response
)
return self._show_step_user(errors)
except ClientResponseError as ex:
if ex.status == 401:
errors[CONF_ACCESS_TOKEN] = "token_unauthorized"
elif ex.status == 403:
errors[CONF_ACCESS_TOKEN] = "token_forbidden"
else:
errors["base"] = "app_setup_error"
_LOGGER.exception("Unexpected error setting up the SmartApp")
return self._show_step_user(errors)
except Exception: # pylint:disable=broad-except
errors["base"] = "app_setup_error"
_LOGGER.exception("Unexpected error setting up the SmartApp")
return self._show_step_user(errors)
return await self.async_step_wait_install()
async def async_step_wait_install(self, user_input=None):
"""Wait for SmartApp installation."""
errors = {}
if user_input is None:
return self._show_step_wait_install(errors)
# Find installed apps that were authorized
installed_apps = self.hass.data[DOMAIN][CONF_INSTALLED_APPS].copy()
if not installed_apps:
errors["base"] = "app_not_installed"
return self._show_step_wait_install(errors)
self.hass.data[DOMAIN][CONF_INSTALLED_APPS].clear()
# Enrich the data
for installed_app in installed_apps:
installed_app[CONF_APP_ID] = self.app_id
installed_app[CONF_ACCESS_TOKEN] = self.access_token
installed_app[CONF_OAUTH_CLIENT_ID] = self.oauth_client_id
installed_app[CONF_OAUTH_CLIENT_SECRET] = self.oauth_client_secret
# User may have installed the SmartApp in more than one SmartThings
# location. Config flows are created for the additional installations
for installed_app in installed_apps[1:]:
self.hass.async_create_task(
self.hass.config_entries.flow.async_init(
DOMAIN, context={"source": "install"}, data=installed_app
)
)
# Create config entity for the first one.
return await self.async_step_install(installed_apps[0])
def _show_step_user(self, errors):
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{vol.Required(CONF_ACCESS_TOKEN, default=self.access_token): str}
),
errors=errors,
description_placeholders={
"token_url": "https://account.smartthings.com/tokens",
"component_url": "https://www.home-assistant.io/integrations/smartthings/",
},
)
def _show_step_wait_install(self, errors):
return self.async_show_form(step_id="wait_install", errors=errors)
async def async_step_install(self, data=None):
"""
Create a config entry at completion of a flow.
Launched when the user completes the flow or when the SmartApp
is installed into an additional location.
"""
if not self.api:
# Launched from the SmartApp install event handler
self.api = SmartThings(
async_get_clientsession(self.hass), data[CONF_ACCESS_TOKEN]
)
location = await self.api.location(data[CONF_LOCATION_ID])
return self.async_create_entry(title=location.name, data=data)