Add cloudhook support to SmartThings component (#21905)
* Add support for Nabu Casa cloudhooks * Added tests to cover cloudhook creation and removal * Remove cloud dependencypull/21933/head
parent
3fd6aa0ba9
commit
c401f35a43
|
@ -25,9 +25,10 @@ from .const import (
|
||||||
TOKEN_REFRESH_INTERVAL)
|
TOKEN_REFRESH_INTERVAL)
|
||||||
from .smartapp import (
|
from .smartapp import (
|
||||||
setup_smartapp, setup_smartapp_endpoint, smartapp_sync_subscriptions,
|
setup_smartapp, setup_smartapp_endpoint, smartapp_sync_subscriptions,
|
||||||
validate_installed_app)
|
unload_smartapp_endpoint, validate_installed_app,
|
||||||
|
validate_webhook_requirements)
|
||||||
|
|
||||||
REQUIREMENTS = ['pysmartapp==0.3.1', 'pysmartthings==0.6.7']
|
REQUIREMENTS = ['pysmartapp==0.3.2', 'pysmartthings==0.6.7']
|
||||||
DEPENDENCIES = ['webhook']
|
DEPENDENCIES = ['webhook']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -64,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
|
||||||
"""Initialize config entry which represents an installed SmartApp."""
|
"""Initialize config entry which represents an installed SmartApp."""
|
||||||
from pysmartthings import SmartThings
|
from pysmartthings import SmartThings
|
||||||
|
|
||||||
if not hass.config.api.base_url.lower().startswith('https://'):
|
if not validate_webhook_requirements(hass):
|
||||||
_LOGGER.warning("The 'base_url' of the 'http' component must be "
|
_LOGGER.warning("The 'base_url' of the 'http' component must be "
|
||||||
"configured and start with 'https://'")
|
"configured and start with 'https://'")
|
||||||
return False
|
return False
|
||||||
|
@ -200,8 +201,9 @@ async def async_remove_entry(
|
||||||
|
|
||||||
# Remove the app if not referenced by other entries, which if already
|
# Remove the app if not referenced by other entries, which if already
|
||||||
# removed raises a 403 error.
|
# removed raises a 403 error.
|
||||||
|
all_entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
app_id = entry.data[CONF_APP_ID]
|
app_id = entry.data[CONF_APP_ID]
|
||||||
app_count = sum(1 for entry in hass.config_entries.async_entries(DOMAIN)
|
app_count = sum(1 for entry in all_entries
|
||||||
if entry.data[CONF_APP_ID] == app_id)
|
if entry.data[CONF_APP_ID] == app_id)
|
||||||
if app_count > 1:
|
if app_count > 1:
|
||||||
_LOGGER.debug("App %s was not removed because it is in use by other"
|
_LOGGER.debug("App %s was not removed because it is in use by other"
|
||||||
|
@ -218,6 +220,9 @@ async def async_remove_entry(
|
||||||
raise
|
raise
|
||||||
_LOGGER.debug("Removed app %s", app_id)
|
_LOGGER.debug("Removed app %s", app_id)
|
||||||
|
|
||||||
|
if len(all_entries) == 1:
|
||||||
|
await unload_smartapp_endpoint(hass)
|
||||||
|
|
||||||
|
|
||||||
class DeviceBroker:
|
class DeviceBroker:
|
||||||
"""Manages an individual SmartThings config entry."""
|
"""Manages an individual SmartThings config entry."""
|
||||||
|
|
|
@ -13,7 +13,8 @@ from .const import (
|
||||||
CONF_LOCATION_ID, CONF_OAUTH_CLIENT_ID, CONF_OAUTH_CLIENT_SECRET, DOMAIN,
|
CONF_LOCATION_ID, CONF_OAUTH_CLIENT_ID, CONF_OAUTH_CLIENT_SECRET, DOMAIN,
|
||||||
VAL_UID_MATCHER)
|
VAL_UID_MATCHER)
|
||||||
from .smartapp import (
|
from .smartapp import (
|
||||||
create_app, find_app, setup_smartapp, setup_smartapp_endpoint, update_app)
|
create_app, find_app, setup_smartapp, setup_smartapp_endpoint, update_app,
|
||||||
|
validate_webhook_requirements)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -56,10 +57,6 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow):
|
||||||
from pysmartthings import APIResponseError, AppOAuth, SmartThings
|
from pysmartthings import APIResponseError, AppOAuth, SmartThings
|
||||||
|
|
||||||
errors = {}
|
errors = {}
|
||||||
if not self.hass.config.api.base_url.lower().startswith('https://'):
|
|
||||||
errors['base'] = "base_url_not_https"
|
|
||||||
return self._show_step_user(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_user(errors)
|
||||||
|
|
||||||
|
@ -81,6 +78,10 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow):
|
||||||
# Setup end-point
|
# Setup end-point
|
||||||
await setup_smartapp_endpoint(self.hass)
|
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:
|
try:
|
||||||
app = await find_app(self.hass, self.api)
|
app = await find_app(self.hass, self.api)
|
||||||
if app:
|
if app:
|
||||||
|
|
|
@ -8,6 +8,7 @@ APP_OAUTH_SCOPES = [
|
||||||
]
|
]
|
||||||
APP_NAME_PREFIX = 'homeassistant.'
|
APP_NAME_PREFIX = 'homeassistant.'
|
||||||
CONF_APP_ID = 'app_id'
|
CONF_APP_ID = 'app_id'
|
||||||
|
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_INSTALLED_APPS = 'installed_apps'
|
||||||
CONF_INSTANCE_ID = 'instance_id'
|
CONF_INSTANCE_ID = 'instance_id'
|
||||||
|
|
|
@ -8,11 +8,12 @@ callbacks when device states change.
|
||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
|
from urllib.parse import urlparse
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from homeassistant.components import webhook
|
from homeassistant.components import cloud, webhook
|
||||||
from homeassistant.const import CONF_WEBHOOK_ID
|
from homeassistant.const import CONF_WEBHOOK_ID
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.dispatcher import (
|
from homeassistant.helpers.dispatcher import (
|
||||||
|
@ -21,9 +22,10 @@ from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
APP_NAME_PREFIX, APP_OAUTH_CLIENT_NAME, APP_OAUTH_SCOPES, CONF_APP_ID,
|
APP_NAME_PREFIX, APP_OAUTH_CLIENT_NAME, APP_OAUTH_SCOPES, CONF_APP_ID,
|
||||||
CONF_INSTALLED_APP_ID, CONF_INSTALLED_APPS, CONF_INSTANCE_ID,
|
CONF_CLOUDHOOK_URL, CONF_INSTALLED_APP_ID, CONF_INSTALLED_APPS,
|
||||||
CONF_LOCATION_ID, CONF_REFRESH_TOKEN, DATA_BROKERS, DATA_MANAGER, DOMAIN,
|
CONF_INSTANCE_ID, CONF_LOCATION_ID, CONF_REFRESH_TOKEN, DATA_BROKERS,
|
||||||
SETTINGS_INSTANCE_ID, SIGNAL_SMARTAPP_PREFIX, STORAGE_KEY, STORAGE_VERSION)
|
DATA_MANAGER, DOMAIN, SETTINGS_INSTANCE_ID, SIGNAL_SMARTAPP_PREFIX,
|
||||||
|
STORAGE_KEY, STORAGE_VERSION)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -59,15 +61,39 @@ async def validate_installed_app(api, installed_app_id: str):
|
||||||
return installed_app
|
return installed_app
|
||||||
|
|
||||||
|
|
||||||
|
def validate_webhook_requirements(hass: HomeAssistantType) -> bool:
|
||||||
|
"""Ensure HASS is setup properly to receive webhooks."""
|
||||||
|
if cloud.async_active_subscription(hass):
|
||||||
|
return True
|
||||||
|
return get_webhook_url(hass).lower().startswith('https://')
|
||||||
|
|
||||||
|
|
||||||
|
def get_webhook_url(hass: HomeAssistantType) -> str:
|
||||||
|
"""
|
||||||
|
Get the URL of the webhook.
|
||||||
|
|
||||||
|
Return the cloudhook if available, otherwise local webhook.
|
||||||
|
"""
|
||||||
|
cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL]
|
||||||
|
if cloud.async_active_subscription(hass) and cloudhook_url is not None:
|
||||||
|
return cloudhook_url
|
||||||
|
return webhook.async_generate_url(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID])
|
||||||
|
|
||||||
|
|
||||||
def _get_app_template(hass: HomeAssistantType):
|
def _get_app_template(hass: HomeAssistantType):
|
||||||
from pysmartthings import APP_TYPE_WEBHOOK, CLASSIFICATION_AUTOMATION
|
from pysmartthings import APP_TYPE_WEBHOOK, CLASSIFICATION_AUTOMATION
|
||||||
|
|
||||||
|
endpoint = "at " + hass.config.api.base_url
|
||||||
|
cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL]
|
||||||
|
if cloudhook_url is not None:
|
||||||
|
endpoint = "via Nabu Casa"
|
||||||
|
description = "{} {}".format(hass.config.location_name, endpoint)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'app_name': APP_NAME_PREFIX + str(uuid4()),
|
'app_name': APP_NAME_PREFIX + str(uuid4()),
|
||||||
'display_name': 'Home Assistant',
|
'display_name': 'Home Assistant',
|
||||||
'description': "Home Assistant at " + hass.config.api.base_url,
|
'description': description,
|
||||||
'webhook_target_url': webhook.async_generate_url(
|
'webhook_target_url': get_webhook_url(hass),
|
||||||
hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]),
|
|
||||||
'app_type': APP_TYPE_WEBHOOK,
|
'app_type': APP_TYPE_WEBHOOK,
|
||||||
'single_instance': True,
|
'single_instance': True,
|
||||||
'classifications': [CLASSIFICATION_AUTOMATION]
|
'classifications': [CLASSIFICATION_AUTOMATION]
|
||||||
|
@ -162,33 +188,80 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType):
|
||||||
# Create config
|
# Create config
|
||||||
config = {
|
config = {
|
||||||
CONF_INSTANCE_ID: str(uuid4()),
|
CONF_INSTANCE_ID: str(uuid4()),
|
||||||
CONF_WEBHOOK_ID: webhook.generate_secret()
|
CONF_WEBHOOK_ID: webhook.generate_secret(),
|
||||||
|
CONF_CLOUDHOOK_URL: None
|
||||||
}
|
}
|
||||||
await store.async_save(config)
|
await store.async_save(config)
|
||||||
|
|
||||||
|
# Register webhook
|
||||||
|
webhook.async_register(hass, DOMAIN, 'SmartApp',
|
||||||
|
config[CONF_WEBHOOK_ID], smartapp_webhook)
|
||||||
|
|
||||||
|
# Create webhook if eligible
|
||||||
|
cloudhook_url = config.get(CONF_CLOUDHOOK_URL)
|
||||||
|
if cloudhook_url is None \
|
||||||
|
and cloud.async_active_subscription(hass) \
|
||||||
|
and not hass.config_entries.async_entries(DOMAIN):
|
||||||
|
cloudhook_url = await cloud.async_create_cloudhook(
|
||||||
|
hass, config[CONF_WEBHOOK_ID])
|
||||||
|
config[CONF_CLOUDHOOK_URL] = cloudhook_url
|
||||||
|
await store.async_save(config)
|
||||||
|
_LOGGER.debug("Created cloudhook '%s'", cloudhook_url)
|
||||||
|
|
||||||
# SmartAppManager uses a dispatcher to invoke callbacks when push events
|
# SmartAppManager uses a dispatcher to invoke callbacks when push events
|
||||||
# occur. Use hass' implementation instead of the built-in one.
|
# occur. Use hass' implementation instead of the built-in one.
|
||||||
dispatcher = Dispatcher(
|
dispatcher = Dispatcher(
|
||||||
signal_prefix=SIGNAL_SMARTAPP_PREFIX,
|
signal_prefix=SIGNAL_SMARTAPP_PREFIX,
|
||||||
connect=functools.partial(async_dispatcher_connect, hass),
|
connect=functools.partial(async_dispatcher_connect, hass),
|
||||||
send=functools.partial(async_dispatcher_send, hass))
|
send=functools.partial(async_dispatcher_send, hass))
|
||||||
manager = SmartAppManager(
|
# Path is used in digital signature validation
|
||||||
webhook.async_generate_path(config[CONF_WEBHOOK_ID]),
|
path = urlparse(cloudhook_url).path if cloudhook_url else \
|
||||||
dispatcher=dispatcher)
|
webhook.async_generate_path(config[CONF_WEBHOOK_ID])
|
||||||
|
manager = SmartAppManager(path, dispatcher=dispatcher)
|
||||||
manager.connect_install(functools.partial(smartapp_install, hass))
|
manager.connect_install(functools.partial(smartapp_install, hass))
|
||||||
manager.connect_update(functools.partial(smartapp_update, hass))
|
manager.connect_update(functools.partial(smartapp_update, hass))
|
||||||
manager.connect_uninstall(functools.partial(smartapp_uninstall, hass))
|
manager.connect_uninstall(functools.partial(smartapp_uninstall, hass))
|
||||||
|
|
||||||
webhook.async_register(hass, DOMAIN, 'SmartApp',
|
|
||||||
config[CONF_WEBHOOK_ID], smartapp_webhook)
|
|
||||||
|
|
||||||
hass.data[DOMAIN] = {
|
hass.data[DOMAIN] = {
|
||||||
DATA_MANAGER: manager,
|
DATA_MANAGER: manager,
|
||||||
CONF_INSTANCE_ID: config[CONF_INSTANCE_ID],
|
CONF_INSTANCE_ID: config[CONF_INSTANCE_ID],
|
||||||
DATA_BROKERS: {},
|
DATA_BROKERS: {},
|
||||||
CONF_WEBHOOK_ID: config[CONF_WEBHOOK_ID],
|
CONF_WEBHOOK_ID: config[CONF_WEBHOOK_ID],
|
||||||
|
# Will not be present if not enabled
|
||||||
|
CONF_CLOUDHOOK_URL: config.get(CONF_CLOUDHOOK_URL),
|
||||||
CONF_INSTALLED_APPS: []
|
CONF_INSTALLED_APPS: []
|
||||||
}
|
}
|
||||||
|
_LOGGER.debug("Setup endpoint for %s",
|
||||||
|
cloudhook_url if cloudhook_url else
|
||||||
|
webhook.async_generate_url(hass, config[CONF_WEBHOOK_ID]))
|
||||||
|
|
||||||
|
|
||||||
|
async def unload_smartapp_endpoint(hass: HomeAssistantType):
|
||||||
|
"""Tear down the component configuration."""
|
||||||
|
if DOMAIN not in hass.data:
|
||||||
|
return
|
||||||
|
# Remove the cloudhook if it was created
|
||||||
|
cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL]
|
||||||
|
if cloudhook_url and cloud.async_is_logged_in(hass):
|
||||||
|
await cloud.async_delete_cloudhook(
|
||||||
|
hass, hass.data[DOMAIN][CONF_WEBHOOK_ID])
|
||||||
|
# Remove cloudhook from storage
|
||||||
|
store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||||
|
await store.async_save({
|
||||||
|
CONF_INSTANCE_ID: hass.data[DOMAIN][CONF_INSTANCE_ID],
|
||||||
|
CONF_WEBHOOK_ID: hass.data[DOMAIN][CONF_WEBHOOK_ID],
|
||||||
|
CONF_CLOUDHOOK_URL: None
|
||||||
|
})
|
||||||
|
_LOGGER.debug("Cloudhook '%s' was removed", cloudhook_url)
|
||||||
|
# Remove the webhook
|
||||||
|
webhook.async_unregister(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID])
|
||||||
|
# Disconnect all brokers
|
||||||
|
for broker in hass.data[DOMAIN][DATA_BROKERS].values():
|
||||||
|
broker.disconnect()
|
||||||
|
# Remove all handlers from manager
|
||||||
|
hass.data[DOMAIN][DATA_MANAGER].dispatcher.disconnect_all()
|
||||||
|
# Remove the component data
|
||||||
|
hass.data.pop(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
async def smartapp_sync_subscriptions(
|
async def smartapp_sync_subscriptions(
|
||||||
|
@ -285,6 +358,9 @@ async def smartapp_install(hass: HomeAssistantType, req, resp, app):
|
||||||
# Store the data where the flow can find it
|
# Store the data where the flow can find it
|
||||||
hass.data[DOMAIN][CONF_INSTALLED_APPS].append(install_data)
|
hass.data[DOMAIN][CONF_INSTALLED_APPS].append(install_data)
|
||||||
|
|
||||||
|
_LOGGER.debug("Installed SmartApp '%s' under parent app '%s'",
|
||||||
|
req.installed_app_id, app.app_id)
|
||||||
|
|
||||||
|
|
||||||
async def smartapp_update(hass: HomeAssistantType, req, resp, app):
|
async def smartapp_update(hass: HomeAssistantType, req, resp, app):
|
||||||
"""
|
"""
|
||||||
|
@ -301,7 +377,7 @@ async def smartapp_update(hass: HomeAssistantType, req, resp, app):
|
||||||
entry.data[CONF_REFRESH_TOKEN] = req.refresh_token
|
entry.data[CONF_REFRESH_TOKEN] = req.refresh_token
|
||||||
hass.config_entries.async_update_entry(entry)
|
hass.config_entries.async_update_entry(entry)
|
||||||
|
|
||||||
_LOGGER.debug("SmartApp '%s' under parent app '%s' was updated",
|
_LOGGER.debug("Updated SmartApp '%s' under parent app '%s'",
|
||||||
req.installed_app_id, app.app_id)
|
req.installed_app_id, app.app_id)
|
||||||
|
|
||||||
|
|
||||||
|
@ -316,12 +392,13 @@ async def smartapp_uninstall(hass: HomeAssistantType, req, resp, app):
|
||||||
req.installed_app_id),
|
req.installed_app_id),
|
||||||
None)
|
None)
|
||||||
if entry:
|
if entry:
|
||||||
_LOGGER.debug("SmartApp '%s' under parent app '%s' was removed",
|
|
||||||
req.installed_app_id, app.app_id)
|
|
||||||
# Add as job not needed because the current coroutine was invoked
|
# Add as job not needed because the current coroutine was invoked
|
||||||
# from the dispatcher and is not being awaited.
|
# from the dispatcher and is not being awaited.
|
||||||
await hass.config_entries.async_remove(entry.entry_id)
|
await hass.config_entries.async_remove(entry.entry_id)
|
||||||
|
|
||||||
|
_LOGGER.debug("Uninstalled SmartApp '%s' under parent app '%s'",
|
||||||
|
req.installed_app_id, app.app_id)
|
||||||
|
|
||||||
|
|
||||||
async def smartapp_webhook(hass: HomeAssistantType, webhook_id: str, request):
|
async def smartapp_webhook(hass: HomeAssistantType, webhook_id: str, request):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1268,7 +1268,7 @@ pysher==1.0.1
|
||||||
pysma==0.3.1
|
pysma==0.3.1
|
||||||
|
|
||||||
# homeassistant.components.smartthings
|
# homeassistant.components.smartthings
|
||||||
pysmartapp==0.3.1
|
pysmartapp==0.3.2
|
||||||
|
|
||||||
# homeassistant.components.smartthings
|
# homeassistant.components.smartthings
|
||||||
pysmartthings==0.6.7
|
pysmartthings==0.6.7
|
||||||
|
|
|
@ -224,7 +224,7 @@ pyps4-homeassistant==0.4.8
|
||||||
pyqwikswitch==0.8
|
pyqwikswitch==0.8
|
||||||
|
|
||||||
# homeassistant.components.smartthings
|
# homeassistant.components.smartthings
|
||||||
pysmartapp==0.3.1
|
pysmartapp==0.3.2
|
||||||
|
|
||||||
# homeassistant.components.smartthings
|
# homeassistant.components.smartthings
|
||||||
pysmartthings==0.6.7
|
pysmartthings==0.6.7
|
||||||
|
|
|
@ -85,7 +85,8 @@ def app_fixture(hass, config_file):
|
||||||
'appType': 'WEBHOOK_SMART_APP',
|
'appType': 'WEBHOOK_SMART_APP',
|
||||||
'classifications': [CLASSIFICATION_AUTOMATION],
|
'classifications': [CLASSIFICATION_AUTOMATION],
|
||||||
'displayName': 'Home Assistant',
|
'displayName': 'Home Assistant',
|
||||||
'description': "Home Assistant at " + hass.config.api.base_url,
|
'description':
|
||||||
|
hass.config.location_name + " at " + hass.config.api.base_url,
|
||||||
'singleInstance': True,
|
'singleInstance': True,
|
||||||
'webhookSmartApp': {
|
'webhookSmartApp': {
|
||||||
'targetUrl': webhook.async_generate_url(
|
'targetUrl': webhook.async_generate_url(
|
||||||
|
|
|
@ -6,6 +6,8 @@ from aiohttp import ClientResponseError
|
||||||
from pysmartthings import APIResponseError
|
from pysmartthings import APIResponseError
|
||||||
|
|
||||||
from homeassistant import data_entry_flow
|
from homeassistant import data_entry_flow
|
||||||
|
from homeassistant.components import cloud
|
||||||
|
from homeassistant.components.smartthings import smartapp
|
||||||
from homeassistant.components.smartthings.config_flow import (
|
from homeassistant.components.smartthings.config_flow import (
|
||||||
SmartThingsFlowHandler)
|
SmartThingsFlowHandler)
|
||||||
from homeassistant.components.smartthings.const import (
|
from homeassistant.components.smartthings.const import (
|
||||||
|
@ -41,7 +43,7 @@ async def test_base_url_not_https(hass):
|
||||||
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_import()
|
result = await flow.async_step_user({'access_token': str(uuid4())})
|
||||||
|
|
||||||
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'
|
||||||
|
@ -193,6 +195,38 @@ async def test_app_created_then_show_wait_form(
|
||||||
assert result['step_id'] == 'wait_install'
|
assert result['step_id'] == 'wait_install'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_cloudhook_app_created_then_show_wait_form(
|
||||||
|
hass, app, app_oauth_client, smartthings_mock):
|
||||||
|
"""Test SmartApp is created with a cloudhoko and shows wait form."""
|
||||||
|
# Unload the endpoint so we can reload it under the cloud.
|
||||||
|
await smartapp.unload_smartapp_endpoint(hass)
|
||||||
|
|
||||||
|
mock_async_active_subscription = Mock(return_value=True)
|
||||||
|
mock_create_cloudhook = Mock(return_value=mock_coro(
|
||||||
|
return_value="http://cloud.test"))
|
||||||
|
with patch.object(cloud, 'async_active_subscription',
|
||||||
|
new=mock_async_active_subscription), \
|
||||||
|
patch.object(cloud, 'async_create_cloudhook',
|
||||||
|
new=mock_create_cloudhook):
|
||||||
|
|
||||||
|
await smartapp.setup_smartapp_endpoint(hass)
|
||||||
|
|
||||||
|
flow = SmartThingsFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
smartthings = smartthings_mock.return_value
|
||||||
|
smartthings.apps.return_value = mock_coro(return_value=[])
|
||||||
|
smartthings.create_app.return_value = \
|
||||||
|
mock_coro(return_value=(app, app_oauth_client))
|
||||||
|
smartthings.update_app_settings.return_value = mock_coro()
|
||||||
|
smartthings.update_app_oauth.return_value = mock_coro()
|
||||||
|
|
||||||
|
result = await flow.async_step_user({'access_token': str(uuid4())})
|
||||||
|
|
||||||
|
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result['step_id'] == 'wait_install'
|
||||||
|
assert mock_create_cloudhook.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_app_updated_then_show_wait_form(
|
async def test_app_updated_then_show_wait_form(
|
||||||
hass, app, app_oauth_client, smartthings_mock):
|
hass, app, app_oauth_client, smartthings_mock):
|
||||||
"""Test SmartApp is updated when an existing is already created."""
|
"""Test SmartApp is updated when an existing is already created."""
|
||||||
|
|
|
@ -6,10 +6,11 @@ from aiohttp import ClientConnectionError, ClientResponseError
|
||||||
from pysmartthings import InstalledAppStatus
|
from pysmartthings import InstalledAppStatus
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components import smartthings
|
from homeassistant.components import cloud, smartthings
|
||||||
from homeassistant.components.smartthings.const import (
|
from homeassistant.components.smartthings.const import (
|
||||||
CONF_INSTALLED_APP_ID, CONF_REFRESH_TOKEN, DATA_BROKERS, DOMAIN,
|
CONF_CLOUDHOOK_URL, CONF_INSTALLED_APP_ID, CONF_REFRESH_TOKEN,
|
||||||
EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS)
|
DATA_BROKERS, DOMAIN, EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE,
|
||||||
|
SUPPORTED_PLATFORMS)
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
|
@ -224,6 +225,29 @@ async def test_remove_entry(hass, config_entry, smartthings_mock):
|
||||||
assert api.delete_app.call_count == 1
|
assert api.delete_app.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_remove_entry_cloudhook(hass, config_entry, smartthings_mock):
|
||||||
|
"""Test that the installed app, app, and cloudhook are removed up."""
|
||||||
|
# Arrange
|
||||||
|
setattr(hass.config_entries, '_entries', [config_entry])
|
||||||
|
hass.data[DOMAIN][CONF_CLOUDHOOK_URL] = "https://test.cloud"
|
||||||
|
api = smartthings_mock.return_value
|
||||||
|
api.delete_installed_app.side_effect = lambda _: mock_coro()
|
||||||
|
api.delete_app.side_effect = lambda _: mock_coro()
|
||||||
|
mock_async_is_logged_in = Mock(return_value=True)
|
||||||
|
mock_async_delete_cloudhook = Mock(return_value=mock_coro())
|
||||||
|
# Act
|
||||||
|
with patch.object(cloud, 'async_is_logged_in',
|
||||||
|
new=mock_async_is_logged_in), \
|
||||||
|
patch.object(cloud, 'async_delete_cloudhook',
|
||||||
|
new=mock_async_delete_cloudhook):
|
||||||
|
await smartthings.async_remove_entry(hass, config_entry)
|
||||||
|
# Assert
|
||||||
|
assert api.delete_installed_app.call_count == 1
|
||||||
|
assert api.delete_app.call_count == 1
|
||||||
|
assert mock_async_is_logged_in.call_count == 1
|
||||||
|
assert mock_async_delete_cloudhook.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_remove_entry_app_in_use(hass, config_entry, smartthings_mock):
|
async def test_remove_entry_app_in_use(hass, config_entry, smartthings_mock):
|
||||||
"""Test app is not removed if in use by another config entry."""
|
"""Test app is not removed if in use by another config entry."""
|
||||||
# Arrange
|
# Arrange
|
||||||
|
|
Loading…
Reference in New Issue