Mobile App: Register devices into the registry (#21856)
* Register devices into the registry * Switch to device ID instead of webhook ID * Rearchitect mobile_app to support config entries * Kill DATA_REGISTRATIONS by migrating registrations into config entries * Fix tests * Improve how we get the config_entry_id * Remove single_instance_allowed * Simplify setup_registration * Move webhook registering functions into __init__.py since they are only ever used once * Kill get_registration websocket command * Support description_placeholders in async_abort * Add link to mobile_app implementing apps in abort dialog * Store config entry and device registry entry in hass.data instead of looking it up * Add testing to ensure that the config entry is created at registration * Fix busted async_abort test * Remove unnecessary check for entry is None
This commit is contained in:
parent
62f12d242a
commit
3769f5893a
15 changed files with 254 additions and 212 deletions
14
homeassistant/components/mobile_app/.translations/en.json
Normal file
14
homeassistant/components/mobile_app/.translations/en.json
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "Mobile App",
|
||||||
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"title": "Mobile App",
|
||||||
|
"description": "Do you want to set up the Mobile App component?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"install_app": "Open the mobile app to set up the integration with Home Assistant. See [the docs]({apps_url}) for a list of compatible apps."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,18 @@
|
||||||
"""Integrates Native Apps to Home Assistant."""
|
"""Integrates Native Apps to Home Assistant."""
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_WEBHOOK_ID
|
||||||
|
from homeassistant.components.webhook import async_register as webhook_register
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.helpers.discovery import load_platform
|
||||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||||
|
|
||||||
from .const import (DATA_DELETED_IDS, DATA_REGISTRATIONS, DATA_STORE, DOMAIN,
|
from .const import (ATTR_APP_COMPONENT, ATTR_DEVICE_ID, ATTR_DEVICE_NAME,
|
||||||
STORAGE_KEY, STORAGE_VERSION)
|
ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION,
|
||||||
|
DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, DATA_DEVICES,
|
||||||
|
DATA_STORE, DOMAIN, STORAGE_KEY, STORAGE_VERSION)
|
||||||
|
|
||||||
from .http_api import register_http_handlers
|
from .http_api import RegistrationsView
|
||||||
from .webhook import register_deleted_webhooks, setup_registration
|
from .webhook import handle_webhook
|
||||||
from .websocket_api import register_websocket_handlers
|
from .websocket_api import register_websocket_handlers
|
||||||
|
|
||||||
DEPENDENCIES = ['device_tracker', 'http', 'webhook']
|
DEPENDENCIES = ['device_tracker', 'http', 'webhook']
|
||||||
|
@ -15,24 +22,88 @@ REQUIREMENTS = ['PyNaCl==1.3.0']
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||||
"""Set up the mobile app component."""
|
"""Set up the mobile app component."""
|
||||||
|
hass.data[DOMAIN] = {
|
||||||
|
DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], DATA_DEVICES: {},
|
||||||
|
}
|
||||||
|
|
||||||
store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||||
app_config = await store.async_load()
|
app_config = await store.async_load()
|
||||||
if app_config is None:
|
if app_config is None:
|
||||||
app_config = {DATA_DELETED_IDS: [], DATA_REGISTRATIONS: {}}
|
app_config = {
|
||||||
|
DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], DATA_DEVICES: {},
|
||||||
|
}
|
||||||
|
|
||||||
if hass.data.get(DOMAIN) is None:
|
hass.data[DOMAIN] = app_config
|
||||||
hass.data[DOMAIN] = {DATA_DELETED_IDS: [], DATA_REGISTRATIONS: {}}
|
|
||||||
|
|
||||||
hass.data[DOMAIN][DATA_DELETED_IDS] = app_config.get(DATA_DELETED_IDS, [])
|
|
||||||
hass.data[DOMAIN][DATA_REGISTRATIONS] = app_config.get(DATA_REGISTRATIONS,
|
|
||||||
{})
|
|
||||||
hass.data[DOMAIN][DATA_STORE] = store
|
hass.data[DOMAIN][DATA_STORE] = store
|
||||||
|
|
||||||
for registration in app_config[DATA_REGISTRATIONS].values():
|
hass.http.register_view(RegistrationsView())
|
||||||
setup_registration(hass, store, registration)
|
|
||||||
|
|
||||||
register_http_handlers(hass, store)
|
|
||||||
register_websocket_handlers(hass)
|
register_websocket_handlers(hass)
|
||||||
register_deleted_webhooks(hass, store)
|
|
||||||
|
for deleted_id in hass.data[DOMAIN][DATA_DELETED_IDS]:
|
||||||
|
try:
|
||||||
|
webhook_register(hass, DOMAIN, "Deleted Webhook", deleted_id,
|
||||||
|
handle_webhook)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry):
|
||||||
|
"""Set up a mobile_app entry."""
|
||||||
|
registration = entry.data
|
||||||
|
|
||||||
|
webhook_id = registration[CONF_WEBHOOK_ID]
|
||||||
|
|
||||||
|
hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] = entry
|
||||||
|
|
||||||
|
device_registry = await dr.async_get_registry(hass)
|
||||||
|
|
||||||
|
identifiers = {
|
||||||
|
(ATTR_DEVICE_ID, registration[ATTR_DEVICE_ID]),
|
||||||
|
(CONF_WEBHOOK_ID, registration[CONF_WEBHOOK_ID])
|
||||||
|
}
|
||||||
|
|
||||||
|
device = device_registry.async_get_or_create(
|
||||||
|
config_entry_id=entry.entry_id,
|
||||||
|
identifiers=identifiers,
|
||||||
|
manufacturer=registration[ATTR_MANUFACTURER],
|
||||||
|
model=registration[ATTR_MODEL],
|
||||||
|
name=registration[ATTR_DEVICE_NAME],
|
||||||
|
sw_version=registration[ATTR_OS_VERSION]
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.data[DOMAIN][DATA_DEVICES][webhook_id] = device
|
||||||
|
|
||||||
|
registration_name = 'Mobile App: {}'.format(registration[ATTR_DEVICE_NAME])
|
||||||
|
webhook_register(hass, DOMAIN, registration_name, webhook_id,
|
||||||
|
handle_webhook)
|
||||||
|
|
||||||
|
if ATTR_APP_COMPONENT in registration:
|
||||||
|
load_platform(hass, registration[ATTR_APP_COMPONENT], DOMAIN, {},
|
||||||
|
{DOMAIN: {}})
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@config_entries.HANDLERS.register(DOMAIN)
|
||||||
|
class MobileAppFlowHandler(config_entries.ConfigFlow):
|
||||||
|
"""Handle a Mobile App config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
placeholders = {
|
||||||
|
'apps_url':
|
||||||
|
'https://www.home-assistant.io/components/mobile_app/#apps'
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.async_abort(reason='install_app',
|
||||||
|
description_placeholders=placeholders)
|
||||||
|
|
||||||
|
async def async_step_registration(self, user_input=None):
|
||||||
|
"""Handle a flow initialized during registration."""
|
||||||
|
return self.async_create_entry(title=user_input[ATTR_DEVICE_NAME],
|
||||||
|
data=user_input)
|
||||||
|
|
|
@ -17,8 +17,9 @@ CONF_CLOUDHOOK_URL = 'cloudhook_url'
|
||||||
CONF_SECRET = 'secret'
|
CONF_SECRET = 'secret'
|
||||||
CONF_USER_ID = 'user_id'
|
CONF_USER_ID = 'user_id'
|
||||||
|
|
||||||
|
DATA_CONFIG_ENTRIES = 'config_entries'
|
||||||
DATA_DELETED_IDS = 'deleted_ids'
|
DATA_DELETED_IDS = 'deleted_ids'
|
||||||
DATA_REGISTRATIONS = 'registrations'
|
DATA_DEVICES = 'devices'
|
||||||
DATA_STORE = 'store'
|
DATA_STORE = 'store'
|
||||||
|
|
||||||
ATTR_APP_COMPONENT = 'app_component'
|
ATTR_APP_COMPONENT = 'app_component'
|
||||||
|
@ -26,6 +27,7 @@ ATTR_APP_DATA = 'app_data'
|
||||||
ATTR_APP_ID = 'app_id'
|
ATTR_APP_ID = 'app_id'
|
||||||
ATTR_APP_NAME = 'app_name'
|
ATTR_APP_NAME = 'app_name'
|
||||||
ATTR_APP_VERSION = 'app_version'
|
ATTR_APP_VERSION = 'app_version'
|
||||||
|
ATTR_CONFIG_ENTRY_ID = 'entry_id'
|
||||||
ATTR_DEVICE_ID = 'device_id'
|
ATTR_DEVICE_ID = 'device_id'
|
||||||
ATTR_DEVICE_NAME = 'device_name'
|
ATTR_DEVICE_NAME = 'device_name'
|
||||||
ATTR_MANUFACTURER = 'manufacturer'
|
ATTR_MANUFACTURER = 'manufacturer'
|
||||||
|
@ -52,7 +54,6 @@ ATTR_WEBHOOK_TYPE = 'type'
|
||||||
|
|
||||||
ERR_ENCRYPTION_REQUIRED = 'encryption_required'
|
ERR_ENCRYPTION_REQUIRED = 'encryption_required'
|
||||||
ERR_INVALID_COMPONENT = 'invalid_component'
|
ERR_INVALID_COMPONENT = 'invalid_component'
|
||||||
ERR_SAVE_FAILURE = 'save_failure'
|
|
||||||
|
|
||||||
WEBHOOK_TYPE_CALL_SERVICE = 'call_service'
|
WEBHOOK_TYPE_CALL_SERVICE = 'call_service'
|
||||||
WEBHOOK_TYPE_FIRE_EVENT = 'fire_event'
|
WEBHOOK_TYPE_FIRE_EVENT = 'fire_event'
|
||||||
|
|
|
@ -11,8 +11,7 @@ from homeassistant.helpers.typing import HomeAssistantType
|
||||||
from .const import (ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME,
|
from .const import (ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME,
|
||||||
ATTR_APP_VERSION, ATTR_DEVICE_NAME, ATTR_MANUFACTURER,
|
ATTR_APP_VERSION, ATTR_DEVICE_NAME, ATTR_MANUFACTURER,
|
||||||
ATTR_MODEL, ATTR_OS_VERSION, ATTR_SUPPORTS_ENCRYPTION,
|
ATTR_MODEL, ATTR_OS_VERSION, ATTR_SUPPORTS_ENCRYPTION,
|
||||||
CONF_SECRET, CONF_USER_ID, DATA_DELETED_IDS,
|
CONF_SECRET, CONF_USER_ID, DATA_DELETED_IDS, DOMAIN)
|
||||||
DATA_REGISTRATIONS, DOMAIN)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -125,7 +124,6 @@ def savable_state(hass: HomeAssistantType) -> Dict:
|
||||||
"""Return a clean object containing things that should be saved."""
|
"""Return a clean object containing things that should be saved."""
|
||||||
return {
|
return {
|
||||||
DATA_DELETED_IDS: hass.data[DOMAIN][DATA_DELETED_IDS],
|
DATA_DELETED_IDS: hass.data[DOMAIN][DATA_DELETED_IDS],
|
||||||
DATA_REGISTRATIONS: hass.data[DOMAIN][DATA_REGISTRATIONS]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,29 +8,16 @@ from homeassistant.auth.util import generate_secret
|
||||||
from homeassistant.components.cloud import async_create_cloudhook
|
from homeassistant.components.cloud import async_create_cloudhook
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||||
from homeassistant.const import (HTTP_CREATED, HTTP_INTERNAL_SERVER_ERROR,
|
from homeassistant.const import (HTTP_CREATED, CONF_WEBHOOK_ID)
|
||||||
CONF_WEBHOOK_ID)
|
|
||||||
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
|
||||||
from homeassistant.helpers.storage import Store
|
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
|
||||||
from homeassistant.loader import get_component
|
from homeassistant.loader import get_component
|
||||||
|
|
||||||
from .const import (ATTR_APP_COMPONENT, ATTR_DEVICE_ID,
|
from .const import (ATTR_APP_COMPONENT, ATTR_DEVICE_ID,
|
||||||
ATTR_SUPPORTS_ENCRYPTION, CONF_CLOUDHOOK_URL, CONF_SECRET,
|
ATTR_SUPPORTS_ENCRYPTION, CONF_CLOUDHOOK_URL, CONF_SECRET,
|
||||||
CONF_USER_ID, DATA_REGISTRATIONS, DOMAIN,
|
CONF_USER_ID, DOMAIN, ERR_INVALID_COMPONENT,
|
||||||
ERR_INVALID_COMPONENT, ERR_SAVE_FAILURE,
|
|
||||||
REGISTRATION_SCHEMA)
|
REGISTRATION_SCHEMA)
|
||||||
|
|
||||||
from .helpers import error_response, supports_encryption, savable_state
|
from .helpers import error_response, supports_encryption
|
||||||
|
|
||||||
from .webhook import setup_registration
|
|
||||||
|
|
||||||
|
|
||||||
def register_http_handlers(hass: HomeAssistantType, store: Store) -> bool:
|
|
||||||
"""Register the HTTP handlers/views."""
|
|
||||||
hass.http.register_view(RegistrationsView(store))
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class RegistrationsView(HomeAssistantView):
|
class RegistrationsView(HomeAssistantView):
|
||||||
|
@ -39,10 +26,6 @@ class RegistrationsView(HomeAssistantView):
|
||||||
url = '/api/mobile_app/registrations'
|
url = '/api/mobile_app/registrations'
|
||||||
name = 'api:mobile_app:register'
|
name = 'api:mobile_app:register'
|
||||||
|
|
||||||
def __init__(self, store: Store) -> None:
|
|
||||||
"""Initialize the view."""
|
|
||||||
self._store = store
|
|
||||||
|
|
||||||
@RequestDataValidator(REGISTRATION_SCHEMA)
|
@RequestDataValidator(REGISTRATION_SCHEMA)
|
||||||
async def post(self, request: Request, data: Dict) -> Response:
|
async def post(self, request: Request, data: Dict) -> Response:
|
||||||
"""Handle the POST request for registration."""
|
"""Handle the POST request for registration."""
|
||||||
|
@ -79,16 +62,10 @@ class RegistrationsView(HomeAssistantView):
|
||||||
|
|
||||||
data[CONF_USER_ID] = request['hass_user'].id
|
data[CONF_USER_ID] = request['hass_user'].id
|
||||||
|
|
||||||
hass.data[DOMAIN][DATA_REGISTRATIONS][webhook_id] = data
|
ctx = {'source': 'registration'}
|
||||||
|
await hass.async_create_task(
|
||||||
try:
|
hass.config_entries.flow.async_init(DOMAIN, context=ctx,
|
||||||
await self._store.async_save(savable_state(hass))
|
data=data))
|
||||||
except HomeAssistantError:
|
|
||||||
return error_response(ERR_SAVE_FAILURE,
|
|
||||||
"Error saving registration",
|
|
||||||
status=HTTP_INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
setup_registration(hass, self._store, data)
|
|
||||||
|
|
||||||
return self.json({
|
return self.json({
|
||||||
CONF_CLOUDHOOK_URL: data.get(CONF_CLOUDHOOK_URL),
|
CONF_CLOUDHOOK_URL: data.get(CONF_CLOUDHOOK_URL),
|
||||||
|
|
14
homeassistant/components/mobile_app/strings.json
Normal file
14
homeassistant/components/mobile_app/strings.json
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "Mobile App",
|
||||||
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"title": "Mobile App",
|
||||||
|
"description": "Do you want to set up the Mobile App component?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"install_app": "Open the mobile app to set up the integration with Home Assistant. See [the docs]({apps_url}) for a list of compatible apps."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,5 @@
|
||||||
"""Webhook handlers for mobile_app."""
|
"""Webhook handlers for mobile_app."""
|
||||||
from functools import partial
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from aiohttp.web import HTTPBadRequest, Response, Request
|
from aiohttp.web import HTTPBadRequest, Response, Request
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -10,27 +8,24 @@ from homeassistant.components.device_tracker import (ATTR_ATTRIBUTES,
|
||||||
ATTR_DEV_ID,
|
ATTR_DEV_ID,
|
||||||
DOMAIN as DT_DOMAIN,
|
DOMAIN as DT_DOMAIN,
|
||||||
SERVICE_SEE as DT_SEE)
|
SERVICE_SEE as DT_SEE)
|
||||||
from homeassistant.components.webhook import async_register as webhook_register
|
|
||||||
|
|
||||||
from homeassistant.const import (ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA,
|
from homeassistant.const import (ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA,
|
||||||
CONF_WEBHOOK_ID, HTTP_BAD_REQUEST)
|
CONF_WEBHOOK_ID, HTTP_BAD_REQUEST)
|
||||||
from homeassistant.core import EventOrigin
|
from homeassistant.core import EventOrigin
|
||||||
from homeassistant.exceptions import (HomeAssistantError, ServiceNotFound,
|
from homeassistant.exceptions import (ServiceNotFound, TemplateError)
|
||||||
TemplateError)
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.template import attach
|
from homeassistant.helpers.template import attach
|
||||||
from homeassistant.helpers.discovery import load_platform
|
|
||||||
from homeassistant.helpers.storage import Store
|
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from .const import (ATTR_ALTITUDE, ATTR_APP_COMPONENT, ATTR_BATTERY,
|
from .const import (ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID,
|
||||||
ATTR_COURSE, ATTR_DEVICE_ID, ATTR_DEVICE_NAME,
|
ATTR_DEVICE_NAME, ATTR_EVENT_DATA, ATTR_EVENT_TYPE,
|
||||||
ATTR_EVENT_DATA, ATTR_EVENT_TYPE, ATTR_GPS,
|
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME,
|
||||||
ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, ATTR_SPEED,
|
ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, ATTR_SPEED,
|
||||||
ATTR_SUPPORTS_ENCRYPTION, ATTR_TEMPLATE,
|
ATTR_SUPPORTS_ENCRYPTION, ATTR_TEMPLATE,
|
||||||
ATTR_TEMPLATE_VARIABLES, ATTR_VERTICAL_ACCURACY,
|
ATTR_TEMPLATE_VARIABLES, ATTR_VERTICAL_ACCURACY,
|
||||||
ATTR_WEBHOOK_DATA, ATTR_WEBHOOK_ENCRYPTED,
|
ATTR_WEBHOOK_DATA, ATTR_WEBHOOK_ENCRYPTED,
|
||||||
ATTR_WEBHOOK_ENCRYPTED_DATA, ATTR_WEBHOOK_TYPE,
|
ATTR_WEBHOOK_ENCRYPTED_DATA, ATTR_WEBHOOK_TYPE,
|
||||||
CONF_SECRET, DATA_DELETED_IDS, DATA_REGISTRATIONS, DOMAIN,
|
CONF_SECRET, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, DOMAIN,
|
||||||
ERR_ENCRYPTION_REQUIRED, WEBHOOK_PAYLOAD_SCHEMA,
|
ERR_ENCRYPTION_REQUIRED, WEBHOOK_PAYLOAD_SCHEMA,
|
||||||
WEBHOOK_SCHEMAS, WEBHOOK_TYPE_CALL_SERVICE,
|
WEBHOOK_SCHEMAS, WEBHOOK_TYPE_CALL_SERVICE,
|
||||||
WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_RENDER_TEMPLATE,
|
WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_RENDER_TEMPLATE,
|
||||||
|
@ -38,45 +33,24 @@ from .const import (ATTR_ALTITUDE, ATTR_APP_COMPONENT, ATTR_BATTERY,
|
||||||
WEBHOOK_TYPE_UPDATE_REGISTRATION)
|
WEBHOOK_TYPE_UPDATE_REGISTRATION)
|
||||||
|
|
||||||
from .helpers import (_decrypt_payload, empty_okay_response, error_response,
|
from .helpers import (_decrypt_payload, empty_okay_response, error_response,
|
||||||
registration_context, safe_registration, savable_state,
|
registration_context, safe_registration,
|
||||||
webhook_response)
|
webhook_response)
|
||||||
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def register_deleted_webhooks(hass: HomeAssistantType, store: Store):
|
async def handle_webhook(hass: HomeAssistantType, webhook_id: str,
|
||||||
"""Register previously deleted webhook IDs so we can return 410."""
|
request: Request) -> Response:
|
||||||
for deleted_id in hass.data[DOMAIN][DATA_DELETED_IDS]:
|
|
||||||
try:
|
|
||||||
webhook_register(hass, DOMAIN, "Deleted Webhook", deleted_id,
|
|
||||||
partial(handle_webhook, store))
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def setup_registration(hass: HomeAssistantType, store: Store,
|
|
||||||
registration: Dict) -> None:
|
|
||||||
"""Register the webhook for a registration and loads the app component."""
|
|
||||||
registration_name = 'Mobile App: {}'.format(registration[ATTR_DEVICE_NAME])
|
|
||||||
webhook_id = registration[CONF_WEBHOOK_ID]
|
|
||||||
webhook_register(hass, DOMAIN, registration_name, webhook_id,
|
|
||||||
partial(handle_webhook, store))
|
|
||||||
|
|
||||||
if ATTR_APP_COMPONENT in registration:
|
|
||||||
load_platform(hass, registration[ATTR_APP_COMPONENT], DOMAIN, {},
|
|
||||||
{DOMAIN: {}})
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_webhook(store: Store, hass: HomeAssistantType,
|
|
||||||
webhook_id: str, request: Request) -> Response:
|
|
||||||
"""Handle webhook callback."""
|
"""Handle webhook callback."""
|
||||||
if webhook_id in hass.data[DOMAIN][DATA_DELETED_IDS]:
|
if webhook_id in hass.data[DOMAIN][DATA_DELETED_IDS]:
|
||||||
return Response(status=410)
|
return Response(status=410)
|
||||||
|
|
||||||
headers = {}
|
headers = {}
|
||||||
|
|
||||||
registration = hass.data[DOMAIN][DATA_REGISTRATIONS][webhook_id]
|
config_entry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id]
|
||||||
|
|
||||||
|
registration = config_entry.data
|
||||||
|
|
||||||
try:
|
try:
|
||||||
req_data = await request.json()
|
req_data = await request.json()
|
||||||
|
@ -179,13 +153,22 @@ async def handle_webhook(store: Store, hass: HomeAssistantType,
|
||||||
if webhook_type == WEBHOOK_TYPE_UPDATE_REGISTRATION:
|
if webhook_type == WEBHOOK_TYPE_UPDATE_REGISTRATION:
|
||||||
new_registration = {**registration, **data}
|
new_registration = {**registration, **data}
|
||||||
|
|
||||||
hass.data[DOMAIN][DATA_REGISTRATIONS][webhook_id] = new_registration
|
device_registry = await dr.async_get_registry(hass)
|
||||||
|
|
||||||
try:
|
device_registry.async_get_or_create(
|
||||||
await store.async_save(savable_state(hass))
|
config_entry_id=config_entry.entry_id,
|
||||||
except HomeAssistantError as ex:
|
identifiers={
|
||||||
_LOGGER.error("Error updating mobile_app registration: %s", ex)
|
(ATTR_DEVICE_ID, registration[ATTR_DEVICE_ID]),
|
||||||
return empty_okay_response()
|
(CONF_WEBHOOK_ID, registration[CONF_WEBHOOK_ID])
|
||||||
|
},
|
||||||
|
manufacturer=new_registration[ATTR_MANUFACTURER],
|
||||||
|
model=new_registration[ATTR_MODEL],
|
||||||
|
name=new_registration[ATTR_DEVICE_NAME],
|
||||||
|
sw_version=new_registration[ATTR_OS_VERSION]
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.config_entries.async_update_entry(config_entry,
|
||||||
|
data=new_registration)
|
||||||
|
|
||||||
return webhook_response(safe_registration(new_registration),
|
return webhook_response(safe_registration(new_registration),
|
||||||
registration=registration, headers=headers)
|
registration=registration, headers=headers)
|
||||||
|
|
|
@ -17,16 +17,14 @@ from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from .const import (CONF_CLOUDHOOK_URL, CONF_USER_ID, DATA_DELETED_IDS,
|
from .const import (CONF_CLOUDHOOK_URL, CONF_USER_ID, DATA_CONFIG_ENTRIES,
|
||||||
DATA_REGISTRATIONS, DATA_STORE, DOMAIN)
|
DATA_DELETED_IDS, DATA_STORE, DOMAIN)
|
||||||
|
|
||||||
from .helpers import safe_registration, savable_state
|
from .helpers import safe_registration, savable_state
|
||||||
|
|
||||||
|
|
||||||
def register_websocket_handlers(hass: HomeAssistantType) -> bool:
|
def register_websocket_handlers(hass: HomeAssistantType) -> bool:
|
||||||
"""Register the websocket handlers."""
|
"""Register the websocket handlers."""
|
||||||
async_register_command(hass, websocket_get_registration)
|
|
||||||
|
|
||||||
async_register_command(hass, websocket_get_user_registrations)
|
async_register_command(hass, websocket_get_user_registrations)
|
||||||
|
|
||||||
async_register_command(hass, websocket_delete_registration)
|
async_register_command(hass, websocket_delete_registration)
|
||||||
|
@ -34,39 +32,6 @@ def register_websocket_handlers(hass: HomeAssistantType) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ws_require_user()
|
|
||||||
@async_response
|
|
||||||
@websocket_command({
|
|
||||||
vol.Required('type'): 'mobile_app/get_registration',
|
|
||||||
vol.Required(CONF_WEBHOOK_ID): cv.string,
|
|
||||||
})
|
|
||||||
async def websocket_get_registration(
|
|
||||||
hass: HomeAssistantType, connection: ActiveConnection,
|
|
||||||
msg: dict) -> None:
|
|
||||||
"""Return the registration for the given webhook_id."""
|
|
||||||
user = connection.user
|
|
||||||
|
|
||||||
webhook_id = msg.get(CONF_WEBHOOK_ID)
|
|
||||||
if webhook_id is None:
|
|
||||||
connection.send_error(msg['id'], ERR_INVALID_FORMAT,
|
|
||||||
"Webhook ID not provided")
|
|
||||||
return
|
|
||||||
|
|
||||||
registration = hass.data[DOMAIN][DATA_REGISTRATIONS].get(webhook_id)
|
|
||||||
|
|
||||||
if registration is None:
|
|
||||||
connection.send_error(msg['id'], ERR_NOT_FOUND,
|
|
||||||
"Webhook ID not found in storage")
|
|
||||||
return
|
|
||||||
|
|
||||||
if registration[CONF_USER_ID] != user.id and not user.is_admin:
|
|
||||||
return error_message(
|
|
||||||
msg['id'], ERR_UNAUTHORIZED, 'User is not registration owner')
|
|
||||||
|
|
||||||
connection.send_message(
|
|
||||||
result_message(msg['id'], safe_registration(registration)))
|
|
||||||
|
|
||||||
|
|
||||||
@ws_require_user()
|
@ws_require_user()
|
||||||
@async_response
|
@async_response
|
||||||
@websocket_command({
|
@websocket_command({
|
||||||
|
@ -87,7 +52,8 @@ async def websocket_get_user_registrations(
|
||||||
|
|
||||||
user_registrations = []
|
user_registrations = []
|
||||||
|
|
||||||
for registration in hass.data[DOMAIN][DATA_REGISTRATIONS].values():
|
for config_entry in hass.config_entries.async_entries(domain=DOMAIN):
|
||||||
|
registration = config_entry.data
|
||||||
if connection.user.is_admin or registration[CONF_USER_ID] is user_id:
|
if connection.user.is_admin or registration[CONF_USER_ID] is user_id:
|
||||||
user_registrations.append(safe_registration(registration))
|
user_registrations.append(safe_registration(registration))
|
||||||
|
|
||||||
|
@ -113,7 +79,9 @@ async def websocket_delete_registration(hass: HomeAssistantType,
|
||||||
"Webhook ID not provided")
|
"Webhook ID not provided")
|
||||||
return
|
return
|
||||||
|
|
||||||
registration = hass.data[DOMAIN][DATA_REGISTRATIONS].get(webhook_id)
|
config_entry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id]
|
||||||
|
|
||||||
|
registration = config_entry.data
|
||||||
|
|
||||||
if registration is None:
|
if registration is None:
|
||||||
connection.send_error(msg['id'], ERR_NOT_FOUND,
|
connection.send_error(msg['id'], ERR_NOT_FOUND,
|
||||||
|
@ -124,7 +92,7 @@ async def websocket_delete_registration(hass: HomeAssistantType,
|
||||||
return error_message(
|
return error_message(
|
||||||
msg['id'], ERR_UNAUTHORIZED, 'User is not registration owner')
|
msg['id'], ERR_UNAUTHORIZED, 'User is not registration owner')
|
||||||
|
|
||||||
del hass.data[DOMAIN][DATA_REGISTRATIONS][webhook_id]
|
await hass.config_entries.async_remove(config_entry.entry_id)
|
||||||
|
|
||||||
hass.data[DOMAIN][DATA_DELETED_IDS].append(webhook_id)
|
hass.data[DOMAIN][DATA_DELETED_IDS].append(webhook_id)
|
||||||
|
|
||||||
|
|
|
@ -161,6 +161,7 @@ FLOWS = [
|
||||||
'locative',
|
'locative',
|
||||||
'luftdaten',
|
'luftdaten',
|
||||||
'mailgun',
|
'mailgun',
|
||||||
|
'mobile_app',
|
||||||
'mqtt',
|
'mqtt',
|
||||||
'nest',
|
'nest',
|
||||||
'openuv',
|
'openuv',
|
||||||
|
|
|
@ -170,11 +170,13 @@ class FlowHandler:
|
||||||
}
|
}
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_abort(self, *, reason: str) -> Dict:
|
def async_abort(self, *, reason: str,
|
||||||
|
description_placeholders: Optional[Dict] = None) -> Dict:
|
||||||
"""Abort the config flow."""
|
"""Abort the config flow."""
|
||||||
return {
|
return {
|
||||||
'type': RESULT_TYPE_ABORT,
|
'type': RESULT_TYPE_ABORT,
|
||||||
'flow_id': self.flow_id,
|
'flow_id': self.flow_id,
|
||||||
'handler': self.handler,
|
'handler': self.handler,
|
||||||
'reason': reason
|
'reason': reason,
|
||||||
|
'description_placeholders': description_placeholders,
|
||||||
}
|
}
|
||||||
|
|
|
@ -226,6 +226,7 @@ def test_abort(hass, client):
|
||||||
data = yield from resp.json()
|
data = yield from resp.json()
|
||||||
data.pop('flow_id')
|
data.pop('flow_id')
|
||||||
assert data == {
|
assert data == {
|
||||||
|
'description_placeholders': None,
|
||||||
'handler': 'test',
|
'handler': 'test',
|
||||||
'reason': 'bla',
|
'reason': 'bla',
|
||||||
'type': 'abort'
|
'type': 'abort'
|
||||||
|
|
|
@ -2,48 +2,59 @@
|
||||||
# pylint: disable=redefined-outer-name,unused-import
|
# pylint: disable=redefined-outer-name,unused-import
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from tests.common import mock_device_registry
|
||||||
|
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from homeassistant.components.mobile_app.const import (DATA_DELETED_IDS,
|
from homeassistant.components.mobile_app.const import (DATA_CONFIG_ENTRIES,
|
||||||
DATA_REGISTRATIONS,
|
DATA_DELETED_IDS,
|
||||||
CONF_SECRET,
|
DATA_DEVICES,
|
||||||
CONF_USER_ID, DOMAIN,
|
DOMAIN,
|
||||||
STORAGE_KEY,
|
STORAGE_KEY,
|
||||||
STORAGE_VERSION)
|
STORAGE_VERSION)
|
||||||
from homeassistant.const import CONF_WEBHOOK_ID
|
|
||||||
|
from .const import REGISTER, REGISTER_CLEARTEXT
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def webhook_client(hass, aiohttp_client, hass_storage, hass_admin_user):
|
def registry(hass):
|
||||||
|
"""Return a configured device registry."""
|
||||||
|
return mock_device_registry(hass)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def create_registrations(authed_api_client):
|
||||||
|
"""Return two new registrations."""
|
||||||
|
enc_reg = await authed_api_client.post(
|
||||||
|
'/api/mobile_app/registrations', json=REGISTER
|
||||||
|
)
|
||||||
|
|
||||||
|
assert enc_reg.status == 201
|
||||||
|
enc_reg_json = await enc_reg.json()
|
||||||
|
|
||||||
|
clear_reg = await authed_api_client.post(
|
||||||
|
'/api/mobile_app/registrations', json=REGISTER_CLEARTEXT
|
||||||
|
)
|
||||||
|
|
||||||
|
assert clear_reg.status == 201
|
||||||
|
clear_reg_json = await clear_reg.json()
|
||||||
|
|
||||||
|
return (enc_reg_json, clear_reg_json)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def webhook_client(hass, aiohttp_client, hass_storage, hass_admin_user):
|
||||||
"""mobile_app mock client."""
|
"""mobile_app mock client."""
|
||||||
hass_storage[STORAGE_KEY] = {
|
hass_storage[STORAGE_KEY] = {
|
||||||
'version': STORAGE_VERSION,
|
'version': STORAGE_VERSION,
|
||||||
'data': {
|
'data': {
|
||||||
DATA_REGISTRATIONS: {
|
DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], DATA_DEVICES: {},
|
||||||
'mobile_app_test': {
|
|
||||||
CONF_SECRET: '58eb127991594dad934d1584bdee5f27',
|
|
||||||
'supports_encryption': True,
|
|
||||||
CONF_WEBHOOK_ID: 'mobile_app_test',
|
|
||||||
'device_name': 'Test Device',
|
|
||||||
CONF_USER_ID: hass_admin_user.id,
|
|
||||||
},
|
|
||||||
'mobile_app_test_cleartext': {
|
|
||||||
'supports_encryption': False,
|
|
||||||
CONF_WEBHOOK_ID: 'mobile_app_test_cleartext',
|
|
||||||
'device_name': 'Test Device (Cleartext)',
|
|
||||||
CONF_USER_ID: hass_admin_user.id,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
DATA_DELETED_IDS: [],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert hass.loop.run_until_complete(async_setup_component(
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
hass, DOMAIN, {
|
|
||||||
DOMAIN: {}
|
|
||||||
}))
|
|
||||||
|
|
||||||
return hass.loop.run_until_complete(aiohttp_client(hass.http.app))
|
return await aiohttp_client(hass.http.app)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
@ -2,14 +2,15 @@
|
||||||
# pylint: disable=redefined-outer-name,unused-import
|
# pylint: disable=redefined-outer-name,unused-import
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.mobile_app.const import CONF_SECRET
|
from homeassistant.components.mobile_app.const import CONF_SECRET, DOMAIN
|
||||||
from homeassistant.const import CONF_WEBHOOK_ID
|
from homeassistant.const import CONF_WEBHOOK_ID
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from .const import REGISTER, RENDER_TEMPLATE
|
from .const import REGISTER, RENDER_TEMPLATE
|
||||||
from . import authed_api_client # noqa: F401
|
from . import authed_api_client # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
async def test_registration(hass_client, authed_api_client): # noqa: F811
|
async def test_registration(hass, hass_client): # noqa: F811
|
||||||
"""Test that registrations happen."""
|
"""Test that registrations happen."""
|
||||||
try:
|
try:
|
||||||
# pylint: disable=unused-import
|
# pylint: disable=unused-import
|
||||||
|
@ -21,7 +22,11 @@ async def test_registration(hass_client, authed_api_client): # noqa: F811
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
resp = await authed_api_client.post(
|
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
|
||||||
|
api_client = await hass_client()
|
||||||
|
|
||||||
|
resp = await api_client.post(
|
||||||
'/api/mobile_app/registrations', json=REGISTER
|
'/api/mobile_app/registrations', json=REGISTER
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -30,6 +35,20 @@ async def test_registration(hass_client, authed_api_client): # noqa: F811
|
||||||
assert CONF_WEBHOOK_ID in register_json
|
assert CONF_WEBHOOK_ID in register_json
|
||||||
assert CONF_SECRET in register_json
|
assert CONF_SECRET in register_json
|
||||||
|
|
||||||
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
|
||||||
|
assert entries[0].data['app_data'] == REGISTER['app_data']
|
||||||
|
assert entries[0].data['app_id'] == REGISTER['app_id']
|
||||||
|
assert entries[0].data['app_name'] == REGISTER['app_name']
|
||||||
|
assert entries[0].data['app_version'] == REGISTER['app_version']
|
||||||
|
assert entries[0].data['device_name'] == REGISTER['device_name']
|
||||||
|
assert entries[0].data['manufacturer'] == REGISTER['manufacturer']
|
||||||
|
assert entries[0].data['model'] == REGISTER['model']
|
||||||
|
assert entries[0].data['os_name'] == REGISTER['os_name']
|
||||||
|
assert entries[0].data['os_version'] == REGISTER['os_version']
|
||||||
|
assert entries[0].data['supports_encryption'] == \
|
||||||
|
REGISTER['supports_encryption']
|
||||||
|
|
||||||
keylen = SecretBox.KEY_SIZE
|
keylen = SecretBox.KEY_SIZE
|
||||||
key = register_json[CONF_SECRET].encode("utf-8")
|
key = register_json[CONF_SECRET].encode("utf-8")
|
||||||
key = key[:keylen]
|
key = key[:keylen]
|
||||||
|
@ -46,9 +65,7 @@ async def test_registration(hass_client, authed_api_client): # noqa: F811
|
||||||
'encrypted_data': data,
|
'encrypted_data': data,
|
||||||
}
|
}
|
||||||
|
|
||||||
webhook_client = await hass_client()
|
resp = await api_client.post(
|
||||||
|
|
||||||
resp = await webhook_client.post(
|
|
||||||
'/api/webhook/{}'.format(register_json[CONF_WEBHOOK_ID]),
|
'/api/webhook/{}'.format(register_json[CONF_WEBHOOK_ID]),
|
||||||
json=container
|
json=container
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""Webhook tests for mobile_app."""
|
"""Webhook tests for mobile_app."""
|
||||||
# pylint: disable=redefined-outer-name,unused-import
|
# pylint: disable=redefined-outer-name,unused-import
|
||||||
|
import logging
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.mobile_app.const import CONF_SECRET
|
from homeassistant.components.mobile_app.const import CONF_SECRET
|
||||||
|
@ -8,16 +9,20 @@ from homeassistant.core import callback
|
||||||
|
|
||||||
from tests.common import async_mock_service
|
from tests.common import async_mock_service
|
||||||
|
|
||||||
from . import authed_api_client, webhook_client # noqa: F401
|
from . import (authed_api_client, create_registrations, # noqa: F401
|
||||||
|
webhook_client) # noqa: F401
|
||||||
|
|
||||||
from .const import (CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT,
|
from .const import (CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT,
|
||||||
RENDER_TEMPLATE, UPDATE)
|
RENDER_TEMPLATE, UPDATE)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
async def test_webhook_handle_render_template(webhook_client): # noqa: F811
|
|
||||||
|
async def test_webhook_handle_render_template(create_registrations, # noqa: F401, F811, E501
|
||||||
|
webhook_client): # noqa: F811
|
||||||
"""Test that we render templates properly."""
|
"""Test that we render templates properly."""
|
||||||
resp = await webhook_client.post(
|
resp = await webhook_client.post(
|
||||||
'/api/webhook/mobile_app_test_cleartext',
|
'/api/webhook/{}'.format(create_registrations[1]['webhook_id']),
|
||||||
json=RENDER_TEMPLATE
|
json=RENDER_TEMPLATE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,12 +32,13 @@ async def test_webhook_handle_render_template(webhook_client): # noqa: F811
|
||||||
assert json == {'one': 'Hello world'}
|
assert json == {'one': 'Hello world'}
|
||||||
|
|
||||||
|
|
||||||
async def test_webhook_handle_call_services(hass, webhook_client): # noqa: E501 F811
|
async def test_webhook_handle_call_services(hass, create_registrations, # noqa: F401, F811, E501
|
||||||
|
webhook_client): # noqa: E501 F811
|
||||||
"""Test that we call services properly."""
|
"""Test that we call services properly."""
|
||||||
calls = async_mock_service(hass, 'test', 'mobile_app')
|
calls = async_mock_service(hass, 'test', 'mobile_app')
|
||||||
|
|
||||||
resp = await webhook_client.post(
|
resp = await webhook_client.post(
|
||||||
'/api/webhook/mobile_app_test_cleartext',
|
'/api/webhook/{}'.format(create_registrations[1]['webhook_id']),
|
||||||
json=CALL_SERVICE
|
json=CALL_SERVICE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,7 +47,8 @@ async def test_webhook_handle_call_services(hass, webhook_client): # noqa: E501
|
||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_webhook_handle_fire_event(hass, webhook_client): # noqa: F811
|
async def test_webhook_handle_fire_event(hass, create_registrations, # noqa: F401, F811, E501
|
||||||
|
webhook_client): # noqa: F811
|
||||||
"""Test that we can fire events."""
|
"""Test that we can fire events."""
|
||||||
events = []
|
events = []
|
||||||
|
|
||||||
|
@ -53,7 +60,7 @@ async def test_webhook_handle_fire_event(hass, webhook_client): # noqa: F811
|
||||||
hass.bus.async_listen('test_event', store_event)
|
hass.bus.async_listen('test_event', store_event)
|
||||||
|
|
||||||
resp = await webhook_client.post(
|
resp = await webhook_client.post(
|
||||||
'/api/webhook/mobile_app_test_cleartext',
|
'/api/webhook/{}'.format(create_registrations[1]['webhook_id']),
|
||||||
json=FIRE_EVENT
|
json=FIRE_EVENT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -93,10 +100,12 @@ async def test_webhook_update_registration(webhook_client, hass_client): # noqa
|
||||||
assert CONF_SECRET not in update_json
|
assert CONF_SECRET not in update_json
|
||||||
|
|
||||||
|
|
||||||
async def test_webhook_returns_error_incorrect_json(webhook_client, caplog): # noqa: E501 F811
|
async def test_webhook_returns_error_incorrect_json(webhook_client, # noqa: F401, F811, E501
|
||||||
|
create_registrations, # noqa: F401, F811, E501
|
||||||
|
caplog): # noqa: E501 F811
|
||||||
"""Test that an error is returned when JSON is invalid."""
|
"""Test that an error is returned when JSON is invalid."""
|
||||||
resp = await webhook_client.post(
|
resp = await webhook_client.post(
|
||||||
'/api/webhook/mobile_app_test_cleartext',
|
'/api/webhook/{}'.format(create_registrations[1]['webhook_id']),
|
||||||
data='not json'
|
data='not json'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -106,7 +115,8 @@ async def test_webhook_returns_error_incorrect_json(webhook_client, caplog): #
|
||||||
assert 'invalid JSON' in caplog.text
|
assert 'invalid JSON' in caplog.text
|
||||||
|
|
||||||
|
|
||||||
async def test_webhook_handle_decryption(webhook_client): # noqa: F811
|
async def test_webhook_handle_decryption(webhook_client, # noqa: F811
|
||||||
|
create_registrations): # noqa: F401, F811, E501
|
||||||
"""Test that we can encrypt/decrypt properly."""
|
"""Test that we can encrypt/decrypt properly."""
|
||||||
try:
|
try:
|
||||||
# pylint: disable=unused-import
|
# pylint: disable=unused-import
|
||||||
|
@ -119,7 +129,7 @@ async def test_webhook_handle_decryption(webhook_client): # noqa: F811
|
||||||
import json
|
import json
|
||||||
|
|
||||||
keylen = SecretBox.KEY_SIZE
|
keylen = SecretBox.KEY_SIZE
|
||||||
key = "58eb127991594dad934d1584bdee5f27".encode("utf-8")
|
key = create_registrations[0]['secret'].encode("utf-8")
|
||||||
key = key[:keylen]
|
key = key[:keylen]
|
||||||
key = key.ljust(keylen, b'\0')
|
key = key.ljust(keylen, b'\0')
|
||||||
|
|
||||||
|
@ -135,7 +145,7 @@ async def test_webhook_handle_decryption(webhook_client): # noqa: F811
|
||||||
}
|
}
|
||||||
|
|
||||||
resp = await webhook_client.post(
|
resp = await webhook_client.post(
|
||||||
'/api/webhook/mobile_app_test',
|
'/api/webhook/{}'.format(create_registrations[0]['webhook_id']),
|
||||||
json=container
|
json=container
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -151,10 +161,11 @@ async def test_webhook_handle_decryption(webhook_client): # noqa: F811
|
||||||
assert json.loads(decrypted_data) == {'one': 'Hello world'}
|
assert json.loads(decrypted_data) == {'one': 'Hello world'}
|
||||||
|
|
||||||
|
|
||||||
async def test_webhook_requires_encryption(webhook_client): # noqa: F811
|
async def test_webhook_requires_encryption(webhook_client, # noqa: F811
|
||||||
|
create_registrations): # noqa: F401, F811, E501
|
||||||
"""Test that encrypted registrations only accept encrypted data."""
|
"""Test that encrypted registrations only accept encrypted data."""
|
||||||
resp = await webhook_client.post(
|
resp = await webhook_client.post(
|
||||||
'/api/webhook/mobile_app_test',
|
'/api/webhook/{}'.format(create_registrations[0]['webhook_id']),
|
||||||
json=RENDER_TEMPLATE
|
json=RENDER_TEMPLATE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -9,33 +9,6 @@ from . import authed_api_client, setup_ws, webhook_client # noqa: F401
|
||||||
from .const import (CALL_SERVICE, REGISTER)
|
from .const import (CALL_SERVICE, REGISTER)
|
||||||
|
|
||||||
|
|
||||||
async def test_webocket_get_registration(hass, setup_ws, authed_api_client, # noqa: E501 F811
|
|
||||||
hass_ws_client):
|
|
||||||
"""Test get_registration websocket command."""
|
|
||||||
register_resp = await authed_api_client.post(
|
|
||||||
'/api/mobile_app/registrations', json=REGISTER
|
|
||||||
)
|
|
||||||
|
|
||||||
assert register_resp.status == 201
|
|
||||||
register_json = await register_resp.json()
|
|
||||||
assert CONF_WEBHOOK_ID in register_json
|
|
||||||
assert CONF_SECRET in register_json
|
|
||||||
|
|
||||||
client = await hass_ws_client(hass)
|
|
||||||
await client.send_json({
|
|
||||||
'id': 5,
|
|
||||||
'type': 'mobile_app/get_registration',
|
|
||||||
CONF_WEBHOOK_ID: register_json[CONF_WEBHOOK_ID],
|
|
||||||
})
|
|
||||||
|
|
||||||
msg = await client.receive_json()
|
|
||||||
|
|
||||||
assert msg['id'] == 5
|
|
||||||
assert msg['type'] == TYPE_RESULT
|
|
||||||
assert msg['success']
|
|
||||||
assert msg['result']['app_id'] == 'io.homeassistant.mobile_app_test'
|
|
||||||
|
|
||||||
|
|
||||||
async def test_webocket_get_user_registrations(hass, aiohttp_client,
|
async def test_webocket_get_user_registrations(hass, aiohttp_client,
|
||||||
hass_ws_client,
|
hass_ws_client,
|
||||||
hass_read_only_access_token):
|
hass_read_only_access_token):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue