Skip to content

Commit

Permalink
Mobile App: Register devices into the registry (home-assistant#21856)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
robbiet480 authored and balloob committed Mar 15, 2019
1 parent 3fd1e8d commit f7dcfe2
Show file tree
Hide file tree
Showing 15 changed files with 255 additions and 213 deletions.
14 changes: 14 additions & 0 deletions homeassistant/components/mobile_app/.translations/en.json
Original file line number Diff line number Diff line change
@@ -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."
}
}
}
103 changes: 87 additions & 16 deletions homeassistant/components/mobile_app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
"""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 .const import (DATA_DELETED_IDS, DATA_REGISTRATIONS, DATA_STORE, DOMAIN,
STORAGE_KEY, STORAGE_VERSION)
from .const import (ATTR_APP_COMPONENT, ATTR_DEVICE_ID, ATTR_DEVICE_NAME,
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 .webhook import register_deleted_webhooks, setup_registration
from .http_api import RegistrationsView
from .webhook import handle_webhook
from .websocket_api import register_websocket_handlers

DEPENDENCIES = ['device_tracker', 'http', 'webhook']
Expand All @@ -15,24 +22,88 @@

async def async_setup(hass: HomeAssistantType, config: ConfigType):
"""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)
app_config = await store.async_load()
if app_config is None:
app_config = {DATA_DELETED_IDS: [], DATA_REGISTRATIONS: {}}

if hass.data.get(DOMAIN) is None:
hass.data[DOMAIN] = {DATA_DELETED_IDS: [], DATA_REGISTRATIONS: {}}
app_config = {
DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], DATA_DEVICES: {},
}

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] = app_config
hass.data[DOMAIN][DATA_STORE] = store

for registration in app_config[DATA_REGISTRATIONS].values():
setup_registration(hass, store, registration)

register_http_handlers(hass, store)
hass.http.register_view(RegistrationsView())
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


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)
5 changes: 3 additions & 2 deletions homeassistant/components/mobile_app/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@
CONF_SECRET = 'secret'
CONF_USER_ID = 'user_id'

DATA_CONFIG_ENTRIES = 'config_entries'
DATA_DELETED_IDS = 'deleted_ids'
DATA_REGISTRATIONS = 'registrations'
DATA_DEVICES = 'devices'
DATA_STORE = 'store'

ATTR_APP_COMPONENT = 'app_component'
ATTR_APP_DATA = 'app_data'
ATTR_APP_ID = 'app_id'
ATTR_APP_NAME = 'app_name'
ATTR_APP_VERSION = 'app_version'
ATTR_CONFIG_ENTRY_ID = 'entry_id'
ATTR_DEVICE_ID = 'device_id'
ATTR_DEVICE_NAME = 'device_name'
ATTR_MANUFACTURER = 'manufacturer'
Expand All @@ -52,7 +54,6 @@

ERR_ENCRYPTION_REQUIRED = 'encryption_required'
ERR_INVALID_COMPONENT = 'invalid_component'
ERR_SAVE_FAILURE = 'save_failure'

WEBHOOK_TYPE_CALL_SERVICE = 'call_service'
WEBHOOK_TYPE_FIRE_EVENT = 'fire_event'
Expand Down
4 changes: 1 addition & 3 deletions homeassistant/components/mobile_app/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
from .const import (ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME,
ATTR_APP_VERSION, ATTR_DEVICE_NAME, ATTR_MANUFACTURER,
ATTR_MODEL, ATTR_OS_VERSION, ATTR_SUPPORTS_ENCRYPTION,
CONF_SECRET, CONF_USER_ID, DATA_DELETED_IDS,
DATA_REGISTRATIONS, DOMAIN)
CONF_SECRET, CONF_USER_ID, DATA_DELETED_IDS, DOMAIN)

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -125,7 +124,6 @@ def savable_state(hass: HomeAssistantType) -> Dict:
"""Return a clean object containing things that should be saved."""
return {
DATA_DELETED_IDS: hass.data[DOMAIN][DATA_DELETED_IDS],
DATA_REGISTRATIONS: hass.data[DOMAIN][DATA_REGISTRATIONS]
}


Expand Down
37 changes: 7 additions & 30 deletions homeassistant/components/mobile_app/http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,16 @@
from homeassistant.components.cloud import async_create_cloudhook
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.const import (HTTP_CREATED, HTTP_INTERNAL_SERVER_ERROR,
CONF_WEBHOOK_ID)
from homeassistant.const import (HTTP_CREATED, 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 .const import (ATTR_APP_COMPONENT, ATTR_DEVICE_ID,
ATTR_SUPPORTS_ENCRYPTION, CONF_CLOUDHOOK_URL, CONF_SECRET,
CONF_USER_ID, DATA_REGISTRATIONS, DOMAIN,
ERR_INVALID_COMPONENT, ERR_SAVE_FAILURE,
CONF_USER_ID, DOMAIN, ERR_INVALID_COMPONENT,
REGISTRATION_SCHEMA)

from .helpers import error_response, supports_encryption, savable_state

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
from .helpers import error_response, supports_encryption


class RegistrationsView(HomeAssistantView):
Expand All @@ -39,10 +26,6 @@ class RegistrationsView(HomeAssistantView):
url = '/api/mobile_app/registrations'
name = 'api:mobile_app:register'

def __init__(self, store: Store) -> None:
"""Initialize the view."""
self._store = store

@RequestDataValidator(REGISTRATION_SCHEMA)
async def post(self, request: Request, data: Dict) -> Response:
"""Handle the POST request for registration."""
Expand Down Expand Up @@ -79,16 +62,10 @@ async def post(self, request: Request, data: Dict) -> Response:

data[CONF_USER_ID] = request['hass_user'].id

hass.data[DOMAIN][DATA_REGISTRATIONS][webhook_id] = data

try:
await self._store.async_save(savable_state(hass))
except HomeAssistantError:
return error_response(ERR_SAVE_FAILURE,
"Error saving registration",
status=HTTP_INTERNAL_SERVER_ERROR)

setup_registration(hass, self._store, data)
ctx = {'source': 'registration'}
await hass.async_create_task(
hass.config_entries.flow.async_init(DOMAIN, context=ctx,
data=data))

return self.json({
CONF_CLOUDHOOK_URL: data.get(CONF_CLOUDHOOK_URL),
Expand Down
14 changes: 14 additions & 0 deletions homeassistant/components/mobile_app/strings.json
Original file line number Diff line number Diff line change
@@ -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."
}
}
}
75 changes: 29 additions & 46 deletions homeassistant/components/mobile_app/webhook.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""Webhook handlers for mobile_app."""
from functools import partial
import logging
from typing import Dict

from aiohttp.web import HTTPBadRequest, Response, Request
import voluptuous as vol
Expand All @@ -10,73 +8,49 @@
ATTR_DEV_ID,
DOMAIN as DT_DOMAIN,
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,
CONF_WEBHOOK_ID, HTTP_BAD_REQUEST)
from homeassistant.core import EventOrigin
from homeassistant.exceptions import (HomeAssistantError, ServiceNotFound,
TemplateError)
from homeassistant.exceptions import (ServiceNotFound, TemplateError)
from homeassistant.helpers import device_registry as dr
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 .const import (ATTR_ALTITUDE, ATTR_APP_COMPONENT, ATTR_BATTERY,
ATTR_COURSE, ATTR_DEVICE_ID, ATTR_DEVICE_NAME,
ATTR_EVENT_DATA, ATTR_EVENT_TYPE, ATTR_GPS,
ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, ATTR_SPEED,
from .const import (ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID,
ATTR_DEVICE_NAME, ATTR_EVENT_DATA, ATTR_EVENT_TYPE,
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME,
ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, ATTR_SPEED,
ATTR_SUPPORTS_ENCRYPTION, ATTR_TEMPLATE,
ATTR_TEMPLATE_VARIABLES, ATTR_VERTICAL_ACCURACY,
ATTR_WEBHOOK_DATA, ATTR_WEBHOOK_ENCRYPTED,
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,
WEBHOOK_SCHEMAS, WEBHOOK_TYPE_CALL_SERVICE,
WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_RENDER_TEMPLATE,
WEBHOOK_TYPE_UPDATE_LOCATION,
WEBHOOK_TYPE_UPDATE_REGISTRATION)

from .helpers import (_decrypt_payload, empty_okay_response, error_response,
registration_context, safe_registration, savable_state,
registration_context, safe_registration,
webhook_response)


_LOGGER = logging.getLogger(__name__)


def register_deleted_webhooks(hass: HomeAssistantType, store: Store):
"""Register previously deleted webhook IDs so we can return 410."""
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:
async def handle_webhook(hass: HomeAssistantType, webhook_id: str,
request: Request) -> Response:
"""Handle webhook callback."""
if webhook_id in hass.data[DOMAIN][DATA_DELETED_IDS]:
return Response(status=410)

headers = {}

registration = hass.data[DOMAIN][DATA_REGISTRATIONS][webhook_id]
config_entry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id]

registration = config_entry.data

try:
req_data = await request.json()
Expand Down Expand Up @@ -179,13 +153,22 @@ async def handle_webhook(store: Store, hass: HomeAssistantType,
if webhook_type == WEBHOOK_TYPE_UPDATE_REGISTRATION:
new_registration = {**registration, **data}

hass.data[DOMAIN][DATA_REGISTRATIONS][webhook_id] = new_registration

try:
await store.async_save(savable_state(hass))
except HomeAssistantError as ex:
_LOGGER.error("Error updating mobile_app registration: %s", ex)
return empty_okay_response()
device_registry = await dr.async_get_registry(hass)

device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={
(ATTR_DEVICE_ID, registration[ATTR_DEVICE_ID]),
(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),
registration=registration, headers=headers)
Loading

0 comments on commit f7dcfe2

Please sign in to comment.