Update SmartThings config flow to be entirely UI based (#34163)
* bump pysmartthings 0.7.1 * Update config flow to use UI * Code review comments and fix for resetting oauth client * Replace html with markdown
This commit is contained in:
parent
bf33169627
commit
075030f15a
10 changed files with 543 additions and 382 deletions
|
@ -1,28 +1,39 @@
|
||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"error": {
|
"title": "SmartThings",
|
||||||
"app_not_installed": "Please ensure you have installed and authorized the Home Assistant SmartApp and try again.",
|
|
||||||
"app_setup_error": "Unable to setup the SmartApp. Please try again.",
|
|
||||||
"base_url_not_https": "The `base_url` for the `http` component must be configured and start with `https://`.",
|
|
||||||
"token_already_setup": "The token has already been setup.",
|
|
||||||
"token_forbidden": "The token does not have the required OAuth scopes.",
|
|
||||||
"token_invalid_format": "The token must be in the UID/GUID format",
|
|
||||||
"token_unauthorized": "The token is invalid or no longer authorized.",
|
|
||||||
"webhook_error": "SmartThings could not validate the endpoint configured in `base_url`. Please review the component requirements."
|
|
||||||
},
|
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
|
"title": "Confirm Callback URL",
|
||||||
|
"description": "SmartThings will be configured to send push updates to Home Assistant at:\n> {webhook_url}\n\nIf this is not correct, please update your configuration, restart Home Assistant, and try again."
|
||||||
|
},
|
||||||
|
"pat": {
|
||||||
|
"title": "Enter Personal Access Token",
|
||||||
|
"description": "Please enter a SmartThings [Personal Access Token]({token_url}) that has been created per the [instructions]({component_url}). This will be used to create the Home Assistant integration within your SmartThings account.",
|
||||||
"data": {
|
"data": {
|
||||||
"access_token": "Access Token"
|
"access_token": "Access Token"
|
||||||
},
|
|
||||||
"description": "Please enter a SmartThings [Personal Access Token]({token_url}) that has been created per the [instructions]({component_url}).",
|
|
||||||
"title": "Enter Personal Access Token"
|
|
||||||
},
|
|
||||||
"wait_install": {
|
|
||||||
"description": "Please install the Home Assistant SmartApp in at least one location and click submit.",
|
|
||||||
"title": "Install SmartApp"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "SmartThings"
|
"select_location": {
|
||||||
|
"title": "Select Location",
|
||||||
|
"description": "Please select the SmartThings Location you wish to add to Home Assistant. We will then open a new window and ask you to login and authorize installation of the Home Assistant integration into the selected location.",
|
||||||
|
"data": {
|
||||||
|
"location_id": "Location"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authorize": {
|
||||||
|
"title": "Authorize Home Assistant"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"invalid_webhook_url": "Home Assistant is not configured correctly to receive updates from SmartThings. The webhook URL is invalid:\n> {webhook_url}\n\nPlease update your configuration per the [instructions]({component_url}), restart Home Assistant, and try again.",
|
||||||
|
"no_available_locations": "There are no available SmartThings Locations to setup in Home Assistant."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"token_invalid_format": "The token must be in the UID/GUID format",
|
||||||
|
"token_unauthorized": "The token is invalid or no longer authorized.",
|
||||||
|
"token_forbidden": "The token does not have the required OAuth scopes.",
|
||||||
|
"app_setup_error": "Unable to setup the SmartApp. Please try again.",
|
||||||
|
"webhook_error": "SmartThings could not validate the webhook URL. Please ensure the webhook URL is reachable from the internet and try again."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,26 +3,30 @@ import logging
|
||||||
|
|
||||||
from aiohttp import ClientResponseError
|
from aiohttp import ClientResponseError
|
||||||
from pysmartthings import APIResponseError, AppOAuth, SmartThings
|
from pysmartthings import APIResponseError, AppOAuth, SmartThings
|
||||||
|
from pysmartthings.installedapp import format_install_url
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_FORBIDDEN
|
from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_FORBIDDEN
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
# pylint: disable=unused-import
|
||||||
from .const import (
|
from .const import (
|
||||||
APP_OAUTH_CLIENT_NAME,
|
APP_OAUTH_CLIENT_NAME,
|
||||||
APP_OAUTH_SCOPES,
|
APP_OAUTH_SCOPES,
|
||||||
CONF_APP_ID,
|
CONF_APP_ID,
|
||||||
CONF_INSTALLED_APPS,
|
CONF_INSTALLED_APP_ID,
|
||||||
CONF_LOCATION_ID,
|
CONF_LOCATION_ID,
|
||||||
CONF_OAUTH_CLIENT_ID,
|
CONF_OAUTH_CLIENT_ID,
|
||||||
CONF_OAUTH_CLIENT_SECRET,
|
CONF_OAUTH_CLIENT_SECRET,
|
||||||
|
CONF_REFRESH_TOKEN,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
VAL_UID_MATCHER,
|
VAL_UID_MATCHER,
|
||||||
)
|
)
|
||||||
from .smartapp import (
|
from .smartapp import (
|
||||||
create_app,
|
create_app,
|
||||||
find_app,
|
find_app,
|
||||||
|
get_webhook_url,
|
||||||
setup_smartapp,
|
setup_smartapp,
|
||||||
setup_smartapp_endpoint,
|
setup_smartapp_endpoint,
|
||||||
update_app,
|
update_app,
|
||||||
|
@ -32,23 +36,8 @@ from .smartapp import (
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@config_entries.HANDLERS.register(DOMAIN)
|
class SmartThingsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
class SmartThingsFlowHandler(config_entries.ConfigFlow):
|
"""Handle configuration of SmartThings integrations."""
|
||||||
"""
|
|
||||||
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
|
VERSION = 2
|
||||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
|
||||||
|
@ -60,55 +49,84 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow):
|
||||||
self.api = None
|
self.api = None
|
||||||
self.oauth_client_secret = None
|
self.oauth_client_secret = None
|
||||||
self.oauth_client_id = None
|
self.oauth_client_id = None
|
||||||
|
self.installed_app_id = None
|
||||||
|
self.refresh_token = None
|
||||||
|
self.location_id = None
|
||||||
|
|
||||||
async def async_step_import(self, user_input=None):
|
async def async_step_import(self, user_input=None):
|
||||||
"""Occurs when a previously entry setup fails and is re-initiated."""
|
"""Occurs when a previously entry setup fails and is re-initiated."""
|
||||||
return await self.async_step_user(user_input)
|
return await self.async_step_user(user_input)
|
||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
async def async_step_user(self, user_input=None):
|
||||||
"""Get access token and validate it."""
|
"""Validate and confirm webhook setup."""
|
||||||
|
await setup_smartapp_endpoint(self.hass)
|
||||||
|
webhook_url = get_webhook_url(self.hass)
|
||||||
|
|
||||||
|
# Abort if the webhook is invalid
|
||||||
|
if not validate_webhook_requirements(self.hass):
|
||||||
|
return self.async_abort(
|
||||||
|
reason="invalid_webhook_url",
|
||||||
|
description_placeholders={
|
||||||
|
"webhook_url": webhook_url,
|
||||||
|
"component_url": "https://www.home-assistant.io/integrations/smartthings/",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Show the confirmation
|
||||||
|
if user_input is None:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", description_placeholders={"webhook_url": webhook_url},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Show the next screen
|
||||||
|
return await self.async_step_pat()
|
||||||
|
|
||||||
|
async def async_step_pat(self, user_input=None):
|
||||||
|
"""Get the Personal Access Token and validate it."""
|
||||||
errors = {}
|
errors = {}
|
||||||
if user_input is None or CONF_ACCESS_TOKEN not in user_input:
|
if user_input is None or CONF_ACCESS_TOKEN not in user_input:
|
||||||
return self._show_step_user(errors)
|
return self._show_step_pat(errors)
|
||||||
|
|
||||||
self.access_token = user_input.get(CONF_ACCESS_TOKEN, "")
|
self.access_token = user_input[CONF_ACCESS_TOKEN]
|
||||||
self.api = SmartThings(async_get_clientsession(self.hass), self.access_token)
|
|
||||||
|
|
||||||
# Ensure token is a UUID
|
# Ensure token is a UUID
|
||||||
if not VAL_UID_MATCHER.match(self.access_token):
|
if not VAL_UID_MATCHER.match(self.access_token):
|
||||||
errors[CONF_ACCESS_TOKEN] = "token_invalid_format"
|
errors[CONF_ACCESS_TOKEN] = "token_invalid_format"
|
||||||
return self._show_step_user(errors)
|
return self._show_step_pat(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
|
# Setup end-point
|
||||||
await setup_smartapp_endpoint(self.hass)
|
self.api = SmartThings(async_get_clientsession(self.hass), self.access_token)
|
||||||
|
|
||||||
if not validate_webhook_requirements(self.hass):
|
|
||||||
errors["base"] = "base_url_not_https"
|
|
||||||
return self._show_step_user(errors)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app = await find_app(self.hass, self.api)
|
app = await find_app(self.hass, self.api)
|
||||||
if app:
|
if app:
|
||||||
await app.refresh() # load all attributes
|
await app.refresh() # load all attributes
|
||||||
await update_app(self.hass, app)
|
await update_app(self.hass, app)
|
||||||
|
# Find an existing entry to copy the oauth client
|
||||||
|
existing = next(
|
||||||
|
(
|
||||||
|
entry
|
||||||
|
for entry in self._async_current_entries()
|
||||||
|
if entry.data[CONF_APP_ID] == app.app_id
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
self.oauth_client_id = existing.data[CONF_OAUTH_CLIENT_ID]
|
||||||
|
self.oauth_client_secret = existing.data[CONF_OAUTH_CLIENT_SECRET]
|
||||||
|
else:
|
||||||
# Get oauth client id/secret by regenerating it
|
# Get oauth client id/secret by regenerating it
|
||||||
app_oauth = AppOAuth(app.app_id)
|
app_oauth = AppOAuth(app.app_id)
|
||||||
app_oauth.client_name = APP_OAUTH_CLIENT_NAME
|
app_oauth.client_name = APP_OAUTH_CLIENT_NAME
|
||||||
app_oauth.scope.extend(APP_OAUTH_SCOPES)
|
app_oauth.scope.extend(APP_OAUTH_SCOPES)
|
||||||
client = await self.api.generate_app_oauth(app_oauth)
|
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_secret = client.client_secret
|
||||||
self.oauth_client_id = client.client_id
|
self.oauth_client_id = client.client_id
|
||||||
|
else:
|
||||||
|
app, client = await create_app(self.hass, self.api)
|
||||||
|
self.oauth_client_secret = client.client_secret
|
||||||
|
self.oauth_client_id = client.client_id
|
||||||
|
setup_smartapp(self.hass, app)
|
||||||
|
self.app_id = app.app_id
|
||||||
|
|
||||||
except APIResponseError as ex:
|
except APIResponseError as ex:
|
||||||
if ex.is_target_error():
|
if ex.is_target_error():
|
||||||
|
@ -118,58 +136,80 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow):
|
||||||
_LOGGER.exception(
|
_LOGGER.exception(
|
||||||
"API error setting up the SmartApp: %s", ex.raw_error_response
|
"API error setting up the SmartApp: %s", ex.raw_error_response
|
||||||
)
|
)
|
||||||
return self._show_step_user(errors)
|
return self._show_step_pat(errors)
|
||||||
except ClientResponseError as ex:
|
except ClientResponseError as ex:
|
||||||
if ex.status == 401:
|
if ex.status == 401:
|
||||||
errors[CONF_ACCESS_TOKEN] = "token_unauthorized"
|
errors[CONF_ACCESS_TOKEN] = "token_unauthorized"
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Unauthorized error received setting up SmartApp", exc_info=True
|
||||||
|
)
|
||||||
elif ex.status == HTTP_FORBIDDEN:
|
elif ex.status == HTTP_FORBIDDEN:
|
||||||
errors[CONF_ACCESS_TOKEN] = "token_forbidden"
|
errors[CONF_ACCESS_TOKEN] = "token_forbidden"
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Forbidden error received setting up SmartApp", exc_info=True
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
errors["base"] = "app_setup_error"
|
errors["base"] = "app_setup_error"
|
||||||
_LOGGER.exception("Unexpected error setting up the SmartApp")
|
_LOGGER.exception("Unexpected error setting up the SmartApp")
|
||||||
return self._show_step_user(errors)
|
return self._show_step_pat(errors)
|
||||||
except Exception: # pylint:disable=broad-except
|
except Exception: # pylint:disable=broad-except
|
||||||
errors["base"] = "app_setup_error"
|
errors["base"] = "app_setup_error"
|
||||||
_LOGGER.exception("Unexpected error setting up the SmartApp")
|
_LOGGER.exception("Unexpected error setting up the SmartApp")
|
||||||
return self._show_step_user(errors)
|
return self._show_step_pat(errors)
|
||||||
|
|
||||||
return await self.async_step_wait_install()
|
return await self.async_step_select_location()
|
||||||
|
|
||||||
async def async_step_wait_install(self, user_input=None):
|
async def async_step_select_location(self, user_input=None):
|
||||||
"""Wait for SmartApp installation."""
|
"""Ask user to select the location to setup."""
|
||||||
errors = {}
|
if user_input is None or CONF_LOCATION_ID not in user_input:
|
||||||
if user_input is None:
|
# Get available locations
|
||||||
return self._show_step_wait_install(errors)
|
existing_locations = [
|
||||||
|
entry.data[CONF_LOCATION_ID] for entry in self._async_current_entries()
|
||||||
|
]
|
||||||
|
locations = await self.api.locations()
|
||||||
|
locations_options = {
|
||||||
|
location.location_id: location.name
|
||||||
|
for location in locations
|
||||||
|
if location.location_id not in existing_locations
|
||||||
|
}
|
||||||
|
if not locations_options:
|
||||||
|
return self.async_abort(reason="no_available_locations")
|
||||||
|
|
||||||
# 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(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="select_location",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{vol.Required(CONF_LOCATION_ID): vol.In(locations_options)}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.location_id = user_input[CONF_LOCATION_ID]
|
||||||
|
return await self.async_step_authorize()
|
||||||
|
|
||||||
|
async def async_step_authorize(self, user_input=None):
|
||||||
|
"""Wait for the user to authorize the app installation."""
|
||||||
|
user_input = {} if user_input is None else user_input
|
||||||
|
self.installed_app_id = user_input.get(CONF_INSTALLED_APP_ID)
|
||||||
|
self.refresh_token = user_input.get(CONF_REFRESH_TOKEN)
|
||||||
|
if self.installed_app_id is None:
|
||||||
|
# Launch the external setup URL
|
||||||
|
url = format_install_url(self.app_id, self.location_id)
|
||||||
|
return self.async_external_step(step_id="authorize", url=url)
|
||||||
|
|
||||||
|
return self.async_external_step_done(next_step_id="install")
|
||||||
|
|
||||||
|
def _show_step_pat(self, errors):
|
||||||
|
if self.access_token is None:
|
||||||
|
# Get the token from an existing entry to make it easier to setup multiple locations.
|
||||||
|
self.access_token = next(
|
||||||
|
(
|
||||||
|
entry.data.get(CONF_ACCESS_TOKEN)
|
||||||
|
for entry in self._async_current_entries()
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="pat",
|
||||||
data_schema=vol.Schema(
|
data_schema=vol.Schema(
|
||||||
{vol.Required(CONF_ACCESS_TOKEN, default=self.access_token): str}
|
{vol.Required(CONF_ACCESS_TOKEN, default=self.access_token): str}
|
||||||
),
|
),
|
||||||
|
@ -180,21 +220,18 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
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):
|
async def async_step_install(self, data=None):
|
||||||
"""
|
"""Create a config entry at completion of a flow and authorization of the app."""
|
||||||
Create a config entry at completion of a flow.
|
data = {
|
||||||
|
CONF_ACCESS_TOKEN: self.access_token,
|
||||||
Launched when the user completes the flow or when the SmartApp
|
CONF_REFRESH_TOKEN: self.refresh_token,
|
||||||
is installed into an additional location.
|
CONF_OAUTH_CLIENT_ID: self.oauth_client_id,
|
||||||
"""
|
CONF_OAUTH_CLIENT_SECRET: self.oauth_client_secret,
|
||||||
if not self.api:
|
CONF_LOCATION_ID: self.location_id,
|
||||||
# Launched from the SmartApp install event handler
|
CONF_APP_ID: self.app_id,
|
||||||
self.api = SmartThings(
|
CONF_INSTALLED_APP_ID: self.installed_app_id,
|
||||||
async_get_clientsession(self.hass), data[CONF_ACCESS_TOKEN]
|
}
|
||||||
)
|
|
||||||
|
|
||||||
location = await self.api.location(data[CONF_LOCATION_ID])
|
location = await self.api.location(data[CONF_LOCATION_ID])
|
||||||
|
|
||||||
return self.async_create_entry(title=location.name, data=data)
|
return self.async_create_entry(title=location.name, data=data)
|
||||||
|
|
|
@ -8,7 +8,6 @@ APP_NAME_PREFIX = "homeassistant."
|
||||||
CONF_APP_ID = "app_id"
|
CONF_APP_ID = "app_id"
|
||||||
CONF_CLOUDHOOK_URL = "cloudhook_url"
|
CONF_CLOUDHOOK_URL = "cloudhook_url"
|
||||||
CONF_INSTALLED_APP_ID = "installed_app_id"
|
CONF_INSTALLED_APP_ID = "installed_app_id"
|
||||||
CONF_INSTALLED_APPS = "installed_apps"
|
|
||||||
CONF_INSTANCE_ID = "instance_id"
|
CONF_INSTANCE_ID = "instance_id"
|
||||||
CONF_LOCATION_ID = "location_id"
|
CONF_LOCATION_ID = "location_id"
|
||||||
CONF_OAUTH_CLIENT_ID = "client_id"
|
CONF_OAUTH_CLIENT_ID = "client_id"
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"name": "Smartthings",
|
"name": "Smartthings",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/smartthings",
|
"documentation": "https://www.home-assistant.io/integrations/smartthings",
|
||||||
"requirements": ["pysmartapp==0.3.2", "pysmartthings==0.7.0"],
|
"requirements": ["pysmartapp==0.3.2", "pysmartthings==0.7.1"],
|
||||||
"dependencies": ["webhook"],
|
"dependencies": ["webhook"],
|
||||||
"after_dependencies": ["cloud"],
|
"after_dependencies": ["cloud"],
|
||||||
"codeowners": ["@andrewsayre"]
|
"codeowners": ["@andrewsayre"]
|
||||||
|
|
|
@ -36,10 +36,8 @@ from .const import (
|
||||||
APP_NAME_PREFIX,
|
APP_NAME_PREFIX,
|
||||||
APP_OAUTH_CLIENT_NAME,
|
APP_OAUTH_CLIENT_NAME,
|
||||||
APP_OAUTH_SCOPES,
|
APP_OAUTH_SCOPES,
|
||||||
CONF_APP_ID,
|
|
||||||
CONF_CLOUDHOOK_URL,
|
CONF_CLOUDHOOK_URL,
|
||||||
CONF_INSTALLED_APP_ID,
|
CONF_INSTALLED_APP_ID,
|
||||||
CONF_INSTALLED_APPS,
|
|
||||||
CONF_INSTANCE_ID,
|
CONF_INSTANCE_ID,
|
||||||
CONF_LOCATION_ID,
|
CONF_LOCATION_ID,
|
||||||
CONF_REFRESH_TOKEN,
|
CONF_REFRESH_TOKEN,
|
||||||
|
@ -258,7 +256,6 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType):
|
||||||
CONF_WEBHOOK_ID: config[CONF_WEBHOOK_ID],
|
CONF_WEBHOOK_ID: config[CONF_WEBHOOK_ID],
|
||||||
# Will not be present if not enabled
|
# Will not be present if not enabled
|
||||||
CONF_CLOUDHOOK_URL: config.get(CONF_CLOUDHOOK_URL),
|
CONF_CLOUDHOOK_URL: config.get(CONF_CLOUDHOOK_URL),
|
||||||
CONF_INSTALLED_APPS: [],
|
|
||||||
}
|
}
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Setup endpoint for %s",
|
"Setup endpoint for %s",
|
||||||
|
@ -370,40 +367,30 @@ async def smartapp_sync_subscriptions(
|
||||||
|
|
||||||
|
|
||||||
async def smartapp_install(hass: HomeAssistantType, req, resp, app):
|
async def smartapp_install(hass: HomeAssistantType, req, resp, app):
|
||||||
"""
|
"""Handle a SmartApp installation and continue the config flow."""
|
||||||
Handle when a SmartApp is installed by the user into a location.
|
flow = next(
|
||||||
|
|
||||||
Create a config entry representing the installation if this is not
|
|
||||||
the first installation under the account, otherwise store the data
|
|
||||||
for the config flow.
|
|
||||||
"""
|
|
||||||
install_data = {
|
|
||||||
CONF_INSTALLED_APP_ID: req.installed_app_id,
|
|
||||||
CONF_LOCATION_ID: req.location_id,
|
|
||||||
CONF_REFRESH_TOKEN: req.refresh_token,
|
|
||||||
}
|
|
||||||
# App attributes (client id/secret, etc...) are copied from another entry
|
|
||||||
# with the same parent app_id. If one is not found, the install data is
|
|
||||||
# stored for the config flow to retrieve during the wait step.
|
|
||||||
entry = next(
|
|
||||||
(
|
(
|
||||||
entry
|
flow
|
||||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
for flow in hass.config_entries.flow.async_progress()
|
||||||
if entry.data[CONF_APP_ID] == app.app_id
|
if flow["handler"] == DOMAIN
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if entry:
|
if flow is not None:
|
||||||
data = entry.data.copy()
|
await hass.config_entries.flow.async_configure(
|
||||||
data.update(install_data)
|
flow["flow_id"],
|
||||||
# Add as job not needed because the current coroutine was invoked
|
{
|
||||||
# from the dispatcher and is not being awaited.
|
CONF_INSTALLED_APP_ID: req.installed_app_id,
|
||||||
await hass.config_entries.flow.async_init(
|
CONF_LOCATION_ID: req.location_id,
|
||||||
DOMAIN, context={"source": "install"}, data=data
|
CONF_REFRESH_TOKEN: req.refresh_token,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Continued config flow '%s' for SmartApp '%s' under parent app '%s'",
|
||||||
|
flow["flow_id"],
|
||||||
|
req.installed_app_id,
|
||||||
|
app.app_id,
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
# Store the data where the flow can find it
|
|
||||||
hass.data[DOMAIN][CONF_INSTALLED_APPS].append(install_data)
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Installed SmartApp '%s' under parent app '%s'",
|
"Installed SmartApp '%s' under parent app '%s'",
|
||||||
|
@ -413,12 +400,7 @@ async def smartapp_install(hass: HomeAssistantType, req, resp, app):
|
||||||
|
|
||||||
|
|
||||||
async def smartapp_update(hass: HomeAssistantType, req, resp, app):
|
async def smartapp_update(hass: HomeAssistantType, req, resp, app):
|
||||||
"""
|
"""Handle a SmartApp update and either update the entry or continue the flow."""
|
||||||
Handle when a SmartApp is updated (reconfigured) by the user.
|
|
||||||
|
|
||||||
Store the refresh token in the config entry.
|
|
||||||
"""
|
|
||||||
# Update refresh token in config entry
|
|
||||||
entry = next(
|
entry = next(
|
||||||
(
|
(
|
||||||
entry
|
entry
|
||||||
|
@ -431,6 +413,36 @@ async def smartapp_update(hass: HomeAssistantType, req, resp, app):
|
||||||
hass.config_entries.async_update_entry(
|
hass.config_entries.async_update_entry(
|
||||||
entry, data={**entry.data, CONF_REFRESH_TOKEN: req.refresh_token}
|
entry, data={**entry.data, CONF_REFRESH_TOKEN: req.refresh_token}
|
||||||
)
|
)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Updated config entry '%s' for SmartApp '%s' under parent app '%s'",
|
||||||
|
entry.entry_id,
|
||||||
|
req.installed_app_id,
|
||||||
|
app.app_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
flow = next(
|
||||||
|
(
|
||||||
|
flow
|
||||||
|
for flow in hass.config_entries.flow.async_progress()
|
||||||
|
if flow["handler"] == DOMAIN
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if flow is not None:
|
||||||
|
await hass.config_entries.flow.async_configure(
|
||||||
|
flow["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_INSTALLED_APP_ID: req.installed_app_id,
|
||||||
|
CONF_LOCATION_ID: req.location_id,
|
||||||
|
CONF_REFRESH_TOKEN: req.refresh_token,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Continued config flow '%s' for SmartApp '%s' under parent app '%s'",
|
||||||
|
flow["flow_id"],
|
||||||
|
req.installed_app_id,
|
||||||
|
app.app_id,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Updated SmartApp '%s' under parent app '%s'", req.installed_app_id, app.app_id
|
"Updated SmartApp '%s' under parent app '%s'", req.installed_app_id, app.app_id
|
||||||
|
|
|
@ -3,26 +3,37 @@
|
||||||
"title": "SmartThings",
|
"title": "SmartThings",
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
|
"title": "Confirm Callback URL",
|
||||||
|
"description": "SmartThings will be configured to send push updates to Home Assistant at:\n> {webhook_url}\n\nIf this is not correct, please update your configuration, restart Home Assistant, and try again."
|
||||||
|
},
|
||||||
|
"pat": {
|
||||||
"title": "Enter Personal Access Token",
|
"title": "Enter Personal Access Token",
|
||||||
"description": "Please enter a SmartThings [Personal Access Token]({token_url}) that has been created per the [instructions]({component_url}).",
|
"description": "Please enter a SmartThings [Personal Access Token]({token_url}) that has been created per the [instructions]({component_url}). This will be used to create the Home Assistant integration within your SmartThings account.",
|
||||||
"data": {
|
"data": {
|
||||||
"access_token": "Access Token"
|
"access_token": "Access Token"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"wait_install": {
|
"select_location": {
|
||||||
"title": "Install SmartApp",
|
"title": "Select Location",
|
||||||
"description": "Please install the Home Assistant SmartApp in at least one location and click submit."
|
"description": "Please select the SmartThings Location you wish to add to Home Assistant. We will then open a new window and ask you to login and authorize installation of the Home Assistant integration into the selected location.",
|
||||||
|
"data": {
|
||||||
|
"location_id": "Location"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"authorize": {
|
||||||
|
"title": "Authorize Home Assistant"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"invalid_webhook_url": "Home Assistant is not configured correctly to receive updates from SmartThings. The webhook URL is invalid:\n> {webhook_url}\n\nPlease update your configuration per the [instructions]({component_url}), restart Home Assistant, and try again.",
|
||||||
|
"no_available_locations": "There are no available SmartThings Locations to setup in Home Assistant."
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"token_invalid_format": "The token must be in the UID/GUID format",
|
"token_invalid_format": "The token must be in the UID/GUID format",
|
||||||
"token_unauthorized": "The token is invalid or no longer authorized.",
|
"token_unauthorized": "The token is invalid or no longer authorized.",
|
||||||
"token_forbidden": "The token does not have the required OAuth scopes.",
|
"token_forbidden": "The token does not have the required OAuth scopes.",
|
||||||
"token_already_setup": "The token has already been setup.",
|
|
||||||
"app_setup_error": "Unable to setup the SmartApp. Please try again.",
|
"app_setup_error": "Unable to setup the SmartApp. Please try again.",
|
||||||
"app_not_installed": "Please ensure you have installed and authorized the Home Assistant SmartApp and try again.",
|
"webhook_error": "SmartThings could not validate the webhook URL. Please ensure the webhook URL is reachable from the internet and try again."
|
||||||
"base_url_not_https": "The `base_url` for the `http` component must be configured and start with `https://`.",
|
|
||||||
"webhook_error": "SmartThings could not validate the endpoint configured in `base_url`. Please review the component requirements."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1551,7 +1551,7 @@ pysma==0.3.5
|
||||||
pysmartapp==0.3.2
|
pysmartapp==0.3.2
|
||||||
|
|
||||||
# homeassistant.components.smartthings
|
# homeassistant.components.smartthings
|
||||||
pysmartthings==0.7.0
|
pysmartthings==0.7.1
|
||||||
|
|
||||||
# homeassistant.components.smarty
|
# homeassistant.components.smarty
|
||||||
pysmarty==0.8
|
pysmarty==0.8
|
||||||
|
|
|
@ -611,7 +611,7 @@ pysma==0.3.5
|
||||||
pysmartapp==0.3.2
|
pysmartapp==0.3.2
|
||||||
|
|
||||||
# homeassistant.components.smartthings
|
# homeassistant.components.smartthings
|
||||||
pysmartthings==0.7.0
|
pysmartthings==0.7.1
|
||||||
|
|
||||||
# homeassistant.components.soma
|
# homeassistant.components.soma
|
||||||
pysoma==0.0.10
|
pysoma==0.0.10
|
||||||
|
|
|
@ -4,207 +4,266 @@ from uuid import uuid4
|
||||||
from aiohttp import ClientResponseError
|
from aiohttp import ClientResponseError
|
||||||
from asynctest import Mock, patch
|
from asynctest import Mock, patch
|
||||||
from pysmartthings import APIResponseError
|
from pysmartthings import APIResponseError
|
||||||
|
from pysmartthings.installedapp import format_install_url
|
||||||
|
|
||||||
from homeassistant import data_entry_flow
|
from homeassistant import data_entry_flow
|
||||||
from homeassistant.components.smartthings import smartapp
|
from homeassistant.components.smartthings import smartapp
|
||||||
from homeassistant.components.smartthings.config_flow import SmartThingsFlowHandler
|
from homeassistant.components.smartthings.config_flow import SmartThingsFlowHandler
|
||||||
from homeassistant.components.smartthings.const import (
|
from homeassistant.components.smartthings.const import (
|
||||||
|
CONF_APP_ID,
|
||||||
CONF_INSTALLED_APP_ID,
|
CONF_INSTALLED_APP_ID,
|
||||||
CONF_INSTALLED_APPS,
|
|
||||||
CONF_LOCATION_ID,
|
CONF_LOCATION_ID,
|
||||||
|
CONF_OAUTH_CLIENT_ID,
|
||||||
|
CONF_OAUTH_CLIENT_SECRET,
|
||||||
CONF_REFRESH_TOKEN,
|
CONF_REFRESH_TOKEN,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from homeassistant.const import HTTP_FORBIDDEN, HTTP_NOT_FOUND
|
from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_FORBIDDEN, HTTP_NOT_FOUND
|
||||||
from homeassistant.setup import async_setup_component
|
|
||||||
|
|
||||||
from tests.common import MockConfigEntry, mock_coro
|
from tests.common import MockConfigEntry, mock_coro
|
||||||
|
|
||||||
|
|
||||||
async def test_step_user(hass):
|
async def test_step_import(hass):
|
||||||
"""Test the access token form is shown for a user initiated flow."""
|
"""Test import returns user."""
|
||||||
flow = SmartThingsFlowHandler()
|
flow = SmartThingsFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
result = await flow.async_step_user()
|
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
|
||||||
assert result["step_id"] == "user"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_step_init(hass):
|
|
||||||
"""Test the access token form is shown for an init flow."""
|
|
||||||
flow = SmartThingsFlowHandler()
|
|
||||||
flow.hass = hass
|
|
||||||
result = await flow.async_step_import()
|
result = await flow.async_step_import()
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "user"
|
||||||
|
assert result["description_placeholders"][
|
||||||
|
"webhook_url"
|
||||||
|
] == smartapp.get_webhook_url(hass)
|
||||||
|
|
||||||
|
|
||||||
async def test_base_url_not_https(hass):
|
async def test_step_user(hass):
|
||||||
"""Test the base_url parameter starts with https://."""
|
"""Test the webhook confirmation is shown."""
|
||||||
|
flow = SmartThingsFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
result = await flow.async_step_user()
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["description_placeholders"][
|
||||||
|
"webhook_url"
|
||||||
|
] == smartapp.get_webhook_url(hass)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_user_aborts_invalid_webhook(hass):
|
||||||
|
"""Test flow aborts if webhook is invalid."""
|
||||||
hass.config.api.base_url = "http://0.0.0.0"
|
hass.config.api.base_url = "http://0.0.0.0"
|
||||||
flow = SmartThingsFlowHandler()
|
flow = SmartThingsFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
result = await flow.async_step_user({"access_token": str(uuid4())})
|
|
||||||
|
result = await flow.async_step_user()
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "invalid_webhook_url"
|
||||||
|
assert result["description_placeholders"][
|
||||||
|
"webhook_url"
|
||||||
|
] == smartapp.get_webhook_url(hass)
|
||||||
|
assert "component_url" in result["description_placeholders"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_user_advances_to_pat(hass):
|
||||||
|
"""Test user step advances to the pat step."""
|
||||||
|
flow = SmartThingsFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
result = await flow.async_step_user({})
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "pat"
|
||||||
assert result["errors"] == {"base": "base_url_not_https"}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_invalid_token_format(hass):
|
async def test_step_pat(hass):
|
||||||
|
"""Test pat step shows the input form."""
|
||||||
|
flow = SmartThingsFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
result = await flow.async_step_pat()
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "pat"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
assert result["data_schema"]({CONF_ACCESS_TOKEN: ""}) == {CONF_ACCESS_TOKEN: ""}
|
||||||
|
assert "token_url" in result["description_placeholders"]
|
||||||
|
assert "component_url" in result["description_placeholders"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_pat_defaults_token(hass):
|
||||||
|
"""Test pat form defaults the token from another entry."""
|
||||||
|
token = str(uuid4())
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN, data={CONF_ACCESS_TOKEN: token})
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
flow = SmartThingsFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
result = await flow.async_step_pat()
|
||||||
|
|
||||||
|
assert flow.access_token == token
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "pat"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token}
|
||||||
|
assert "token_url" in result["description_placeholders"]
|
||||||
|
assert "component_url" in result["description_placeholders"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_pat_invalid_token(hass):
|
||||||
"""Test an error is shown for invalid token formats."""
|
"""Test an error is shown for invalid token formats."""
|
||||||
flow = SmartThingsFlowHandler()
|
flow = SmartThingsFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
result = await flow.async_step_user({"access_token": "123456789"})
|
token = "123456789"
|
||||||
|
|
||||||
|
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "pat"
|
||||||
|
assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token}
|
||||||
assert result["errors"] == {"access_token": "token_invalid_format"}
|
assert result["errors"] == {"access_token": "token_invalid_format"}
|
||||||
|
assert "token_url" in result["description_placeholders"]
|
||||||
|
assert "component_url" in result["description_placeholders"]
|
||||||
|
|
||||||
|
|
||||||
async def test_token_already_setup(hass):
|
async def test_step_pat_unauthorized(hass, smartthings_mock):
|
||||||
"""Test an error is shown when the token is already setup."""
|
|
||||||
flow = SmartThingsFlowHandler()
|
|
||||||
flow.hass = hass
|
|
||||||
token = str(uuid4())
|
|
||||||
entry = MockConfigEntry(domain=DOMAIN, data={"access_token": token})
|
|
||||||
entry.add_to_hass(hass)
|
|
||||||
|
|
||||||
result = await flow.async_step_user({"access_token": token})
|
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
|
||||||
assert result["step_id"] == "user"
|
|
||||||
assert result["errors"] == {"access_token": "token_already_setup"}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_token_unauthorized(hass, smartthings_mock):
|
|
||||||
"""Test an error is shown when the token is not authorized."""
|
"""Test an error is shown when the token is not authorized."""
|
||||||
flow = SmartThingsFlowHandler()
|
flow = SmartThingsFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
request_info = Mock(real_url="http://example.com")
|
request_info = Mock(real_url="http://example.com")
|
||||||
smartthings_mock.apps.side_effect = ClientResponseError(
|
smartthings_mock.apps.side_effect = ClientResponseError(
|
||||||
request_info=request_info, history=None, status=401
|
request_info=request_info, history=None, status=401
|
||||||
)
|
)
|
||||||
|
token = str(uuid4())
|
||||||
|
|
||||||
result = await flow.async_step_user({"access_token": str(uuid4())})
|
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "pat"
|
||||||
assert result["errors"] == {"access_token": "token_unauthorized"}
|
assert result["errors"] == {CONF_ACCESS_TOKEN: "token_unauthorized"}
|
||||||
|
assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token}
|
||||||
|
|
||||||
|
|
||||||
async def test_token_forbidden(hass, smartthings_mock):
|
async def test_step_pat_forbidden(hass, smartthings_mock):
|
||||||
"""Test an error is shown when the token is forbidden."""
|
"""Test an error is shown when the token is forbidden."""
|
||||||
flow = SmartThingsFlowHandler()
|
flow = SmartThingsFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
request_info = Mock(real_url="http://example.com")
|
request_info = Mock(real_url="http://example.com")
|
||||||
smartthings_mock.apps.side_effect = ClientResponseError(
|
smartthings_mock.apps.side_effect = ClientResponseError(
|
||||||
request_info=request_info, history=None, status=HTTP_FORBIDDEN
|
request_info=request_info, history=None, status=HTTP_FORBIDDEN
|
||||||
)
|
)
|
||||||
|
token = str(uuid4())
|
||||||
|
|
||||||
result = await flow.async_step_user({"access_token": str(uuid4())})
|
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "pat"
|
||||||
assert result["errors"] == {"access_token": "token_forbidden"}
|
assert result["errors"] == {CONF_ACCESS_TOKEN: "token_forbidden"}
|
||||||
|
assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token}
|
||||||
|
|
||||||
|
|
||||||
async def test_webhook_error(hass, smartthings_mock):
|
async def test_step_pat_webhook_error(hass, smartthings_mock):
|
||||||
"""Test an error is when there's an error with the webhook endpoint."""
|
"""Test an error is shown when there's an problem with the webhook endpoint."""
|
||||||
flow = SmartThingsFlowHandler()
|
flow = SmartThingsFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
data = {"error": {}}
|
data = {"error": {}}
|
||||||
request_info = Mock(real_url="http://example.com")
|
request_info = Mock(real_url="http://example.com")
|
||||||
error = APIResponseError(
|
error = APIResponseError(
|
||||||
request_info=request_info, history=None, data=data, status=422
|
request_info=request_info, history=None, data=data, status=422
|
||||||
)
|
)
|
||||||
error.is_target_error = Mock(return_value=True)
|
error.is_target_error = Mock(return_value=True)
|
||||||
|
|
||||||
smartthings_mock.apps.side_effect = error
|
smartthings_mock.apps.side_effect = error
|
||||||
|
token = str(uuid4())
|
||||||
|
|
||||||
result = await flow.async_step_user({"access_token": str(uuid4())})
|
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "pat"
|
||||||
assert result["errors"] == {"base": "webhook_error"}
|
assert result["errors"] == {"base": "webhook_error"}
|
||||||
|
assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token}
|
||||||
|
|
||||||
|
|
||||||
async def test_api_error(hass, smartthings_mock):
|
async def test_step_pat_api_error(hass, smartthings_mock):
|
||||||
"""Test an error is shown when other API errors occur."""
|
"""Test an error is shown when other API errors occur."""
|
||||||
flow = SmartThingsFlowHandler()
|
flow = SmartThingsFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
data = {"error": {}}
|
data = {"error": {}}
|
||||||
request_info = Mock(real_url="http://example.com")
|
request_info = Mock(real_url="http://example.com")
|
||||||
error = APIResponseError(
|
error = APIResponseError(
|
||||||
request_info=request_info, history=None, data=data, status=400
|
request_info=request_info, history=None, data=data, status=400
|
||||||
)
|
)
|
||||||
|
|
||||||
smartthings_mock.apps.side_effect = error
|
smartthings_mock.apps.side_effect = error
|
||||||
|
token = str(uuid4())
|
||||||
|
|
||||||
result = await flow.async_step_user({"access_token": str(uuid4())})
|
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "pat"
|
||||||
assert result["errors"] == {"base": "app_setup_error"}
|
assert result["errors"] == {"base": "app_setup_error"}
|
||||||
|
assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token}
|
||||||
|
|
||||||
|
|
||||||
async def test_unknown_api_error(hass, smartthings_mock):
|
async def test_step_pat_unknown_api_error(hass, smartthings_mock):
|
||||||
"""Test an error is shown when there is an unknown API error."""
|
"""Test an error is shown when there is an unknown API error."""
|
||||||
flow = SmartThingsFlowHandler()
|
flow = SmartThingsFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
request_info = Mock(real_url="http://example.com")
|
request_info = Mock(real_url="http://example.com")
|
||||||
smartthings_mock.apps.side_effect = ClientResponseError(
|
smartthings_mock.apps.side_effect = ClientResponseError(
|
||||||
request_info=request_info, history=None, status=HTTP_NOT_FOUND
|
request_info=request_info, history=None, status=HTTP_NOT_FOUND
|
||||||
)
|
)
|
||||||
|
token = str(uuid4())
|
||||||
|
|
||||||
result = await flow.async_step_user({"access_token": str(uuid4())})
|
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "pat"
|
||||||
assert result["errors"] == {"base": "app_setup_error"}
|
assert result["errors"] == {"base": "app_setup_error"}
|
||||||
|
assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token}
|
||||||
|
|
||||||
|
|
||||||
async def test_unknown_error(hass, smartthings_mock):
|
async def test_step_pat_unknown_error(hass, smartthings_mock):
|
||||||
"""Test an error is shown when there is an unknown API error."""
|
"""Test an error is shown when there is an unknown API error."""
|
||||||
flow = SmartThingsFlowHandler()
|
flow = SmartThingsFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
smartthings_mock.apps.side_effect = Exception("Unknown error")
|
smartthings_mock.apps.side_effect = Exception("Unknown error")
|
||||||
|
token = str(uuid4())
|
||||||
|
|
||||||
result = await flow.async_step_user({"access_token": str(uuid4())})
|
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "pat"
|
||||||
assert result["errors"] == {"base": "app_setup_error"}
|
assert result["errors"] == {"base": "app_setup_error"}
|
||||||
|
assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token}
|
||||||
|
|
||||||
|
|
||||||
async def test_app_created_then_show_wait_form(
|
async def test_step_pat_app_created_webhook(
|
||||||
hass, app, app_oauth_client, smartthings_mock
|
hass, app, app_oauth_client, location, smartthings_mock
|
||||||
):
|
):
|
||||||
"""Test SmartApp is created when one does not exist and shows wait form."""
|
"""Test SmartApp is created when one does not exist and shows location form."""
|
||||||
flow = SmartThingsFlowHandler()
|
flow = SmartThingsFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
smartthings_mock.apps.return_value = []
|
smartthings_mock.apps.return_value = []
|
||||||
smartthings_mock.create_app.return_value = (app, app_oauth_client)
|
smartthings_mock.create_app.return_value = (app, app_oauth_client)
|
||||||
|
smartthings_mock.locations.return_value = [location]
|
||||||
|
token = str(uuid4())
|
||||||
|
|
||||||
result = await flow.async_step_user({"access_token": str(uuid4())})
|
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
|
||||||
|
|
||||||
|
assert flow.access_token == token
|
||||||
|
assert flow.app_id == app.app_id
|
||||||
|
assert flow.oauth_client_secret == app_oauth_client.client_secret
|
||||||
|
assert flow.oauth_client_id == app_oauth_client.client_id
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["step_id"] == "wait_install"
|
assert result["step_id"] == "select_location"
|
||||||
|
|
||||||
|
|
||||||
async def test_cloudhook_app_created_then_show_wait_form(
|
async def test_step_pat_app_created_cloudhook(
|
||||||
hass, app, app_oauth_client, smartthings_mock
|
hass, app, app_oauth_client, location, smartthings_mock
|
||||||
):
|
):
|
||||||
"""Test SmartApp is created with a cloudhoko and shows wait form."""
|
"""Test SmartApp is created with a cloudhook and shows location form."""
|
||||||
hass.config.components.add("cloud")
|
hass.config.components.add("cloud")
|
||||||
|
|
||||||
# Unload the endpoint so we can reload it under the cloud.
|
# Unload the endpoint so we can reload it under the cloud.
|
||||||
|
@ -224,128 +283,159 @@ async def test_cloudhook_app_created_then_show_wait_form(
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
smartthings_mock.apps.return_value = []
|
smartthings_mock.apps.return_value = []
|
||||||
smartthings_mock.create_app.return_value = (app, app_oauth_client)
|
smartthings_mock.create_app.return_value = (app, app_oauth_client)
|
||||||
|
smartthings_mock.locations.return_value = [location]
|
||||||
|
token = str(uuid4())
|
||||||
|
|
||||||
result = await flow.async_step_user({"access_token": str(uuid4())})
|
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
|
||||||
|
|
||||||
|
assert flow.access_token == token
|
||||||
|
assert flow.app_id == app.app_id
|
||||||
|
assert flow.oauth_client_secret == app_oauth_client.client_secret
|
||||||
|
assert flow.oauth_client_id == app_oauth_client.client_id
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["step_id"] == "wait_install"
|
assert result["step_id"] == "select_location"
|
||||||
assert mock_create_cloudhook.call_count == 1
|
assert mock_create_cloudhook.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_app_updated_then_show_wait_form(
|
async def test_step_pat_app_updated_webhook(
|
||||||
hass, app, app_oauth_client, smartthings_mock
|
hass, app, app_oauth_client, location, smartthings_mock
|
||||||
):
|
):
|
||||||
"""Test SmartApp is updated when an existing is already created."""
|
"""Test SmartApp is updated then show location form."""
|
||||||
flow = SmartThingsFlowHandler()
|
flow = SmartThingsFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
|
||||||
smartthings_mock.apps.return_value = [app]
|
smartthings_mock.apps.return_value = [app]
|
||||||
smartthings_mock.generate_app_oauth.return_value = app_oauth_client
|
smartthings_mock.generate_app_oauth.return_value = app_oauth_client
|
||||||
|
smartthings_mock.locations.return_value = [location]
|
||||||
|
token = str(uuid4())
|
||||||
|
|
||||||
result = await flow.async_step_user({"access_token": str(uuid4())})
|
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
|
||||||
|
|
||||||
|
assert flow.access_token == token
|
||||||
|
assert flow.app_id == app.app_id
|
||||||
|
assert flow.oauth_client_secret == app_oauth_client.client_secret
|
||||||
|
assert flow.oauth_client_id == app_oauth_client.client_id
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["step_id"] == "wait_install"
|
assert result["step_id"] == "select_location"
|
||||||
|
|
||||||
|
|
||||||
async def test_wait_form_displayed(hass):
|
async def test_step_pat_app_updated_webhook_from_existing_oauth_client(
|
||||||
"""Test the wait for installation form is displayed."""
|
hass, app, location, smartthings_mock
|
||||||
flow = SmartThingsFlowHandler()
|
|
||||||
flow.hass = hass
|
|
||||||
|
|
||||||
result = await flow.async_step_wait_install(None)
|
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
|
||||||
assert result["step_id"] == "wait_install"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_wait_form_displayed_after_checking(hass, smartthings_mock):
|
|
||||||
"""Test error is shown when the user has not installed the app."""
|
|
||||||
flow = SmartThingsFlowHandler()
|
|
||||||
flow.hass = hass
|
|
||||||
flow.access_token = str(uuid4())
|
|
||||||
|
|
||||||
result = await flow.async_step_wait_install({})
|
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
|
||||||
assert result["step_id"] == "wait_install"
|
|
||||||
assert result["errors"] == {"base": "app_not_installed"}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_config_entry_created_when_installed(
|
|
||||||
hass, location, installed_app, smartthings_mock
|
|
||||||
):
|
):
|
||||||
|
"""Test SmartApp is updated from existing then show location form."""
|
||||||
|
oauth_client_id = str(uuid4())
|
||||||
|
oauth_client_secret = str(uuid4())
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_APP_ID: app.app_id,
|
||||||
|
CONF_OAUTH_CLIENT_ID: oauth_client_id,
|
||||||
|
CONF_OAUTH_CLIENT_SECRET: oauth_client_secret,
|
||||||
|
CONF_LOCATION_ID: str(uuid4()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
flow = SmartThingsFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
smartthings_mock.apps.return_value = [app]
|
||||||
|
smartthings_mock.locations.return_value = [location]
|
||||||
|
token = str(uuid4())
|
||||||
|
|
||||||
|
result = await flow.async_step_pat({CONF_ACCESS_TOKEN: token})
|
||||||
|
|
||||||
|
assert flow.access_token == token
|
||||||
|
assert flow.app_id == app.app_id
|
||||||
|
assert flow.oauth_client_secret == oauth_client_secret
|
||||||
|
assert flow.oauth_client_id == oauth_client_id
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "select_location"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_select_location(hass, location, smartthings_mock):
|
||||||
|
"""Test select location shows form with available locations."""
|
||||||
|
smartthings_mock.locations.return_value = [location]
|
||||||
|
flow = SmartThingsFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
flow.api = smartthings_mock
|
||||||
|
|
||||||
|
result = await flow.async_step_select_location()
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "select_location"
|
||||||
|
assert result["data_schema"]({CONF_LOCATION_ID: location.location_id}) == {
|
||||||
|
CONF_LOCATION_ID: location.location_id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_select_location_aborts(hass, location, smartthings_mock):
|
||||||
|
"""Test select location aborts if no available locations."""
|
||||||
|
smartthings_mock.locations.return_value = [location]
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN, data={CONF_LOCATION_ID: location.location_id}
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
flow = SmartThingsFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
flow.api = smartthings_mock
|
||||||
|
|
||||||
|
result = await flow.async_step_select_location()
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "no_available_locations"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_select_location_advances(hass):
|
||||||
|
"""Test select location aborts if no available locations."""
|
||||||
|
location_id = str(uuid4())
|
||||||
|
app_id = str(uuid4())
|
||||||
|
flow = SmartThingsFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
flow.app_id = app_id
|
||||||
|
|
||||||
|
result = await flow.async_step_select_location({CONF_LOCATION_ID: location_id})
|
||||||
|
|
||||||
|
assert flow.location_id == location_id
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
|
||||||
|
assert result["step_id"] == "authorize"
|
||||||
|
assert result["url"] == format_install_url(app_id, location_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_authorize_advances(hass):
|
||||||
|
"""Test authorize step advances when completed."""
|
||||||
|
installed_app_id = str(uuid4())
|
||||||
|
refresh_token = str(uuid4())
|
||||||
|
flow = SmartThingsFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
result = await flow.async_step_authorize(
|
||||||
|
{CONF_INSTALLED_APP_ID: installed_app_id, CONF_REFRESH_TOKEN: refresh_token}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert flow.installed_app_id == installed_app_id
|
||||||
|
assert flow.refresh_token == refresh_token
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP_DONE
|
||||||
|
assert result["step_id"] == "install"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_install_creates_entry(hass, location, smartthings_mock):
|
||||||
"""Test a config entry is created once the app is installed."""
|
"""Test a config entry is created once the app is installed."""
|
||||||
flow = SmartThingsFlowHandler()
|
flow = SmartThingsFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
flow.access_token = str(uuid4())
|
|
||||||
flow.app_id = installed_app.app_id
|
|
||||||
flow.api = smartthings_mock
|
flow.api = smartthings_mock
|
||||||
|
flow.access_token = str(uuid4())
|
||||||
|
flow.app_id = str(uuid4())
|
||||||
|
flow.installed_app_id = str(uuid4())
|
||||||
|
flow.location_id = location.location_id
|
||||||
flow.oauth_client_id = str(uuid4())
|
flow.oauth_client_id = str(uuid4())
|
||||||
flow.oauth_client_secret = str(uuid4())
|
flow.oauth_client_secret = str(uuid4())
|
||||||
data = {
|
flow.refresh_token = str(uuid4())
|
||||||
CONF_REFRESH_TOKEN: str(uuid4()),
|
|
||||||
CONF_LOCATION_ID: installed_app.location_id,
|
|
||||||
CONF_INSTALLED_APP_ID: installed_app.installed_app_id,
|
|
||||||
}
|
|
||||||
hass.data[DOMAIN][CONF_INSTALLED_APPS].append(data)
|
|
||||||
|
|
||||||
result = await flow.async_step_wait_install({})
|
result = await flow.async_step_install()
|
||||||
|
|
||||||
assert not hass.data[DOMAIN][CONF_INSTALLED_APPS]
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
assert result["data"]["app_id"] == installed_app.app_id
|
assert result["data"]["app_id"] == flow.app_id
|
||||||
assert result["data"]["installed_app_id"] == installed_app.installed_app_id
|
assert result["data"]["installed_app_id"] == flow.installed_app_id
|
||||||
assert result["data"]["location_id"] == installed_app.location_id
|
assert result["data"]["location_id"] == flow.location_id
|
||||||
assert result["data"]["access_token"] == flow.access_token
|
assert result["data"]["access_token"] == flow.access_token
|
||||||
assert result["data"]["refresh_token"] == data[CONF_REFRESH_TOKEN]
|
assert result["data"]["refresh_token"] == flow.refresh_token
|
||||||
assert result["data"]["client_secret"] == flow.oauth_client_secret
|
assert result["data"]["client_secret"] == flow.oauth_client_secret
|
||||||
assert result["data"]["client_id"] == flow.oauth_client_id
|
assert result["data"]["client_id"] == flow.oauth_client_id
|
||||||
assert result["title"] == location.name
|
assert result["title"] == location.name
|
||||||
|
|
||||||
|
|
||||||
async def test_multiple_config_entry_created_when_installed(
|
|
||||||
hass, app, locations, installed_apps, smartthings_mock
|
|
||||||
):
|
|
||||||
"""Test a config entries are created for multiple installs."""
|
|
||||||
assert await async_setup_component(hass, "persistent_notification", {})
|
|
||||||
flow = SmartThingsFlowHandler()
|
|
||||||
flow.hass = hass
|
|
||||||
flow.access_token = str(uuid4())
|
|
||||||
flow.app_id = app.app_id
|
|
||||||
flow.api = smartthings_mock
|
|
||||||
flow.oauth_client_id = str(uuid4())
|
|
||||||
flow.oauth_client_secret = str(uuid4())
|
|
||||||
for installed_app in installed_apps:
|
|
||||||
data = {
|
|
||||||
CONF_REFRESH_TOKEN: str(uuid4()),
|
|
||||||
CONF_LOCATION_ID: installed_app.location_id,
|
|
||||||
CONF_INSTALLED_APP_ID: installed_app.installed_app_id,
|
|
||||||
}
|
|
||||||
hass.data[DOMAIN][CONF_INSTALLED_APPS].append(data)
|
|
||||||
install_data = hass.data[DOMAIN][CONF_INSTALLED_APPS].copy()
|
|
||||||
|
|
||||||
result = await flow.async_step_wait_install({})
|
|
||||||
|
|
||||||
assert not hass.data[DOMAIN][CONF_INSTALLED_APPS]
|
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
|
||||||
assert result["data"]["app_id"] == installed_apps[0].app_id
|
|
||||||
assert result["data"]["installed_app_id"] == installed_apps[0].installed_app_id
|
|
||||||
assert result["data"]["location_id"] == installed_apps[0].location_id
|
|
||||||
assert result["data"]["access_token"] == flow.access_token
|
|
||||||
assert result["data"]["refresh_token"] == install_data[0][CONF_REFRESH_TOKEN]
|
|
||||||
assert result["data"]["client_secret"] == flow.oauth_client_secret
|
|
||||||
assert result["data"]["client_id"] == flow.oauth_client_id
|
|
||||||
assert result["title"] == locations[0].name
|
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
entries = hass.config_entries.async_entries("smartthings")
|
|
||||||
assert len(entries) == 1
|
|
||||||
assert entries[0].data["app_id"] == installed_apps[1].app_id
|
|
||||||
assert entries[0].data["installed_app_id"] == installed_apps[1].installed_app_id
|
|
||||||
assert entries[0].data["location_id"] == installed_apps[1].location_id
|
|
||||||
assert entries[0].data["access_token"] == flow.access_token
|
|
||||||
assert entries[0].data["client_secret"] == flow.oauth_client_secret
|
|
||||||
assert entries[0].data["client_id"] == flow.oauth_client_id
|
|
||||||
assert entries[0].title == locations[1].name
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ from pysmartthings import AppEntity, Capability
|
||||||
from homeassistant.components.smartthings import smartapp
|
from homeassistant.components.smartthings import smartapp
|
||||||
from homeassistant.components.smartthings.const import (
|
from homeassistant.components.smartthings.const import (
|
||||||
CONF_INSTALLED_APP_ID,
|
CONF_INSTALLED_APP_ID,
|
||||||
CONF_INSTALLED_APPS,
|
|
||||||
CONF_LOCATION_ID,
|
CONF_LOCATION_ID,
|
||||||
CONF_REFRESH_TOKEN,
|
CONF_REFRESH_TOKEN,
|
||||||
DATA_MANAGER,
|
DATA_MANAGER,
|
||||||
|
@ -40,11 +39,11 @@ async def test_update_app_updated_needed(hass, app):
|
||||||
assert mock_app.classifications == app.classifications
|
assert mock_app.classifications == app.classifications
|
||||||
|
|
||||||
|
|
||||||
async def test_smartapp_install_store_if_no_other(
|
async def test_smartapp_install_configures_flow(hass):
|
||||||
hass, smartthings_mock, device_factory
|
"""Test install event continues an existing flow."""
|
||||||
):
|
|
||||||
"""Test aborts if no other app was configured already."""
|
|
||||||
# Arrange
|
# Arrange
|
||||||
|
flow_id = str(uuid4())
|
||||||
|
flows = [{"flow_id": flow_id, "handler": DOMAIN}]
|
||||||
app = Mock()
|
app = Mock()
|
||||||
app.app_id = uuid4()
|
app.app_id = uuid4()
|
||||||
request = Mock()
|
request = Mock()
|
||||||
|
@ -52,50 +51,22 @@ async def test_smartapp_install_store_if_no_other(
|
||||||
request.auth_token = str(uuid4())
|
request.auth_token = str(uuid4())
|
||||||
request.location_id = str(uuid4())
|
request.location_id = str(uuid4())
|
||||||
request.refresh_token = str(uuid4())
|
request.refresh_token = str(uuid4())
|
||||||
# Act
|
|
||||||
await smartapp.smartapp_install(hass, request, None, app)
|
|
||||||
# Assert
|
|
||||||
entries = hass.config_entries.async_entries("smartthings")
|
|
||||||
assert not entries
|
|
||||||
data = hass.data[DOMAIN][CONF_INSTALLED_APPS][0]
|
|
||||||
assert data[CONF_REFRESH_TOKEN] == request.refresh_token
|
|
||||||
assert data[CONF_LOCATION_ID] == request.location_id
|
|
||||||
assert data[CONF_INSTALLED_APP_ID] == request.installed_app_id
|
|
||||||
|
|
||||||
|
|
||||||
async def test_smartapp_install_creates_flow(
|
|
||||||
hass, smartthings_mock, config_entry, location, device_factory
|
|
||||||
):
|
|
||||||
"""Test installation creates flow."""
|
|
||||||
# Arrange
|
|
||||||
config_entry.add_to_hass(hass)
|
|
||||||
app = Mock()
|
|
||||||
app.app_id = config_entry.data["app_id"]
|
|
||||||
request = Mock()
|
|
||||||
request.installed_app_id = str(uuid4())
|
|
||||||
request.auth_token = str(uuid4())
|
|
||||||
request.refresh_token = str(uuid4())
|
|
||||||
request.location_id = location.location_id
|
|
||||||
devices = [
|
|
||||||
device_factory("", [Capability.battery, "ping"]),
|
|
||||||
device_factory("", [Capability.switch, Capability.switch_level]),
|
|
||||||
device_factory("", [Capability.switch]),
|
|
||||||
]
|
|
||||||
smartthings_mock.devices.return_value = devices
|
|
||||||
# Act
|
# Act
|
||||||
|
with patch.object(
|
||||||
|
hass.config_entries.flow, "async_progress", return_value=flows
|
||||||
|
), patch.object(hass.config_entries.flow, "async_configure") as configure_mock:
|
||||||
|
|
||||||
await smartapp.smartapp_install(hass, request, None, app)
|
await smartapp.smartapp_install(hass, request, None, app)
|
||||||
# Assert
|
|
||||||
await hass.async_block_till_done()
|
configure_mock.assert_called_once_with(
|
||||||
entries = hass.config_entries.async_entries("smartthings")
|
flow_id,
|
||||||
assert len(entries) == 2
|
{
|
||||||
assert entries[1].data["app_id"] == app.app_id
|
CONF_INSTALLED_APP_ID: request.installed_app_id,
|
||||||
assert entries[1].data["installed_app_id"] == request.installed_app_id
|
CONF_LOCATION_ID: request.location_id,
|
||||||
assert entries[1].data["location_id"] == request.location_id
|
CONF_REFRESH_TOKEN: request.refresh_token,
|
||||||
assert entries[1].data["access_token"] == config_entry.data["access_token"]
|
},
|
||||||
assert entries[1].data["refresh_token"] == request.refresh_token
|
)
|
||||||
assert entries[1].data["client_secret"] == config_entry.data["client_secret"]
|
|
||||||
assert entries[1].data["client_id"] == config_entry.data["client_id"]
|
|
||||||
assert entries[1].title == location.name
|
|
||||||
|
|
||||||
|
|
||||||
async def test_smartapp_update_saves_token(
|
async def test_smartapp_update_saves_token(
|
||||||
|
@ -121,6 +92,36 @@ async def test_smartapp_update_saves_token(
|
||||||
assert entry.data[CONF_REFRESH_TOKEN] == request.refresh_token
|
assert entry.data[CONF_REFRESH_TOKEN] == request.refresh_token
|
||||||
|
|
||||||
|
|
||||||
|
async def test_smartapp_update_configures_flow(hass):
|
||||||
|
"""Test update event continues an existing flow."""
|
||||||
|
# Arrange
|
||||||
|
flow_id = str(uuid4())
|
||||||
|
flows = [{"flow_id": flow_id, "handler": DOMAIN}]
|
||||||
|
app = Mock()
|
||||||
|
app.app_id = uuid4()
|
||||||
|
request = Mock()
|
||||||
|
request.installed_app_id = str(uuid4())
|
||||||
|
request.auth_token = str(uuid4())
|
||||||
|
request.location_id = str(uuid4())
|
||||||
|
request.refresh_token = str(uuid4())
|
||||||
|
|
||||||
|
# Act
|
||||||
|
with patch.object(
|
||||||
|
hass.config_entries.flow, "async_progress", return_value=flows
|
||||||
|
), patch.object(hass.config_entries.flow, "async_configure") as configure_mock:
|
||||||
|
|
||||||
|
await smartapp.smartapp_update(hass, request, None, app)
|
||||||
|
|
||||||
|
configure_mock.assert_called_once_with(
|
||||||
|
flow_id,
|
||||||
|
{
|
||||||
|
CONF_INSTALLED_APP_ID: request.installed_app_id,
|
||||||
|
CONF_LOCATION_ID: request.location_id,
|
||||||
|
CONF_REFRESH_TOKEN: request.refresh_token,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_smartapp_uninstall(hass, config_entry):
|
async def test_smartapp_uninstall(hass, config_entry):
|
||||||
"""Test the config entry is unloaded when the app is uninstalled."""
|
"""Test the config entry is unloaded when the app is uninstalled."""
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue