2019-02-13 20:21:14 +00:00
|
|
|
"""Component to integrate the Home Assistant cloud."""
|
2017-08-29 20:40:08 +00:00
|
|
|
import logging
|
|
|
|
|
|
|
|
import voluptuous as vol
|
|
|
|
|
2019-03-11 19:21:20 +00:00
|
|
|
from homeassistant.components.alexa import smart_home as alexa_sh
|
|
|
|
from homeassistant.components.google_assistant import const as ga_c
|
|
|
|
from homeassistant.const import (
|
|
|
|
CONF_MODE, CONF_NAME, CONF_REGION, EVENT_HOMEASSISTANT_START,
|
|
|
|
EVENT_HOMEASSISTANT_STOP)
|
2019-03-04 03:03:49 +00:00
|
|
|
from homeassistant.core import callback
|
|
|
|
from homeassistant.exceptions import HomeAssistantError
|
2019-03-11 19:21:20 +00:00
|
|
|
from homeassistant.helpers import config_validation as cv, entityfilter
|
2019-03-04 03:03:49 +00:00
|
|
|
from homeassistant.loader import bind_hass
|
|
|
|
from homeassistant.util.aiohttp import MockRequest
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2019-03-11 19:21:20 +00:00
|
|
|
from . import http_api
|
|
|
|
from .const import (
|
|
|
|
CONF_ACME_DIRECTORY_SERVER, CONF_ALEXA, CONF_ALIASES,
|
|
|
|
CONF_CLOUDHOOK_CREATE_URL, CONF_COGNITO_CLIENT_ID, CONF_ENTITY_CONFIG,
|
|
|
|
CONF_FILTER, CONF_GOOGLE_ACTIONS, CONF_GOOGLE_ACTIONS_SYNC_URL,
|
|
|
|
CONF_RELAYER, CONF_REMOTE_API_URL, CONF_SUBSCRIPTION_INFO_URL,
|
|
|
|
CONF_USER_POOL_ID, DOMAIN, MODE_DEV, MODE_PROD)
|
|
|
|
from .prefs import CloudPreferences
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2019-03-13 15:45:26 +00:00
|
|
|
REQUIREMENTS = ['hass-nabucasa==0.5']
|
2019-03-11 19:21:20 +00:00
|
|
|
DEPENDENCIES = ['http']
|
2017-10-29 11:32:02 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2019-03-11 19:21:20 +00:00
|
|
|
DEFAULT_MODE = MODE_PROD
|
|
|
|
|
|
|
|
SERVICE_REMOTE_CONNECT = 'remote_connect'
|
|
|
|
SERVICE_REMOTE_DISCONNECT = 'remote_disconnect'
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2018-01-05 20:33:22 +00:00
|
|
|
|
|
|
|
ALEXA_ENTITY_SCHEMA = vol.Schema({
|
|
|
|
vol.Optional(alexa_sh.CONF_DESCRIPTION): cv.string,
|
|
|
|
vol.Optional(alexa_sh.CONF_DISPLAY_CATEGORIES): cv.string,
|
|
|
|
vol.Optional(alexa_sh.CONF_NAME): cv.string,
|
|
|
|
})
|
|
|
|
|
2018-01-09 23:14:56 +00:00
|
|
|
GOOGLE_ENTITY_SCHEMA = vol.Schema({
|
|
|
|
vol.Optional(CONF_NAME): cv.string,
|
2018-04-30 19:05:29 +00:00
|
|
|
vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]),
|
|
|
|
vol.Optional(ga_c.CONF_ROOM_HINT): cv.string,
|
2018-01-09 23:14:56 +00:00
|
|
|
})
|
|
|
|
|
2018-01-03 18:16:59 +00:00
|
|
|
ASSISTANT_SCHEMA = vol.Schema({
|
2019-03-11 19:21:20 +00:00
|
|
|
vol.Optional(CONF_FILTER, default=dict): entityfilter.FILTER_SCHEMA,
|
2017-11-18 05:10:24 +00:00
|
|
|
})
|
|
|
|
|
2018-01-05 20:33:22 +00:00
|
|
|
ALEXA_SCHEMA = ASSISTANT_SCHEMA.extend({
|
|
|
|
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}
|
|
|
|
})
|
|
|
|
|
2018-01-09 23:14:56 +00:00
|
|
|
GACTIONS_SCHEMA = ASSISTANT_SCHEMA.extend({
|
Add support for locks in google assistant component (#18233)
* Add support for locks in google assistant component
This is supported by the smarthome API, but there is no documentation
for it. This work is based on an article I found with screenshots of
documentation that was erroneously uploaded:
https://www.androidpolice.com/2018/01/17/google-assistant-home-can-now-natively-control-smart-locks-august-vivint-first-supported/
Google Assistant now supports unlocking certain locks - Nest and August
come to mind - via this API, and this commit allows Home Assistant to
do so as well.
Notably, I've added a config option `allow_unlock` that controls
whether we actually honor requests to unlock a lock via the google
assistant. It defaults to false.
Additionally, we add the functionNotSupported error, which makes a
little more sense when we're unable to execute the desired state
transition.
https://developers.google.com/actions/reference/smarthome/errors-exceptions#exception_list
* Fix linter warnings
* Ensure that certain groups are never exposed to cloud entities
For example, the group.all_locks entity - we should probably never
expose this to third party cloud integrations. It's risky.
This is not configurable, but can be extended by adding to the
cloud.const.NEVER_EXPOSED_ENTITIES array.
It's implemented in a modestly hacky fashion, because we determine
whether or not a entity should be excluded/included in several ways.
Notably, we define this array in the top level const.py, to avoid
circular import problems between the cloud/alexa components.
2018-11-06 09:39:10 +00:00
|
|
|
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: GOOGLE_ENTITY_SCHEMA},
|
2018-01-09 23:14:56 +00:00
|
|
|
})
|
|
|
|
|
2019-03-11 19:21:20 +00:00
|
|
|
# pylint: disable=no-value-for-parameter
|
2017-08-29 20:40:08 +00:00
|
|
|
CONFIG_SCHEMA = vol.Schema({
|
|
|
|
DOMAIN: vol.Schema({
|
|
|
|
vol.Optional(CONF_MODE, default=DEFAULT_MODE):
|
2019-03-11 19:21:20 +00:00
|
|
|
vol.In([MODE_DEV, MODE_PROD]),
|
2017-10-15 02:43:14 +00:00
|
|
|
# Change to optional when we include real servers
|
2017-12-16 08:42:25 +00:00
|
|
|
vol.Optional(CONF_COGNITO_CLIENT_ID): str,
|
|
|
|
vol.Optional(CONF_USER_POOL_ID): str,
|
|
|
|
vol.Optional(CONF_REGION): str,
|
|
|
|
vol.Optional(CONF_RELAYER): str,
|
2019-03-11 19:21:20 +00:00
|
|
|
vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): vol.Url(),
|
|
|
|
vol.Optional(CONF_SUBSCRIPTION_INFO_URL): vol.Url(),
|
|
|
|
vol.Optional(CONF_CLOUDHOOK_CREATE_URL): vol.Url(),
|
|
|
|
vol.Optional(CONF_REMOTE_API_URL): vol.Url(),
|
|
|
|
vol.Optional(CONF_ACME_DIRECTORY_SERVER): vol.Url(),
|
2018-01-05 20:33:22 +00:00
|
|
|
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
|
2018-01-09 23:14:56 +00:00
|
|
|
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
|
2017-08-29 20:40:08 +00:00
|
|
|
}),
|
|
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
|
|
|
|
|
2019-03-04 03:03:49 +00:00
|
|
|
class CloudNotAvailable(HomeAssistantError):
|
|
|
|
"""Raised when an action requires the cloud but it's not available."""
|
|
|
|
|
|
|
|
|
|
|
|
@bind_hass
|
|
|
|
@callback
|
2019-03-09 20:15:16 +00:00
|
|
|
def async_is_logged_in(hass) -> bool:
|
2019-03-04 03:03:49 +00:00
|
|
|
"""Test if user is logged in."""
|
|
|
|
return DOMAIN in hass.data and hass.data[DOMAIN].is_logged_in
|
|
|
|
|
|
|
|
|
|
|
|
@bind_hass
|
2019-03-09 20:15:16 +00:00
|
|
|
@callback
|
|
|
|
def async_active_subscription(hass) -> bool:
|
|
|
|
"""Test if user has an active subscription."""
|
|
|
|
return \
|
|
|
|
async_is_logged_in(hass) and not hass.data[DOMAIN].subscription_expired
|
|
|
|
|
|
|
|
|
|
|
|
@bind_hass
|
|
|
|
async def async_create_cloudhook(hass, webhook_id: str) -> str:
|
2019-03-04 03:03:49 +00:00
|
|
|
"""Create a cloudhook."""
|
|
|
|
if not async_is_logged_in(hass):
|
|
|
|
raise CloudNotAvailable
|
|
|
|
|
2019-03-12 18:49:46 +00:00
|
|
|
hook = await hass.data[DOMAIN].cloudhooks.async_create(webhook_id, True)
|
2019-03-09 20:15:16 +00:00
|
|
|
return hook['cloudhook_url']
|
2019-03-04 03:03:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
@bind_hass
|
2019-03-09 20:15:16 +00:00
|
|
|
async def async_delete_cloudhook(hass, webhook_id: str) -> None:
|
2019-03-04 03:03:49 +00:00
|
|
|
"""Delete a cloudhook."""
|
2019-03-09 20:15:16 +00:00
|
|
|
if DOMAIN not in hass.data:
|
2019-03-04 03:03:49 +00:00
|
|
|
raise CloudNotAvailable
|
|
|
|
|
2019-03-09 20:15:16 +00:00
|
|
|
await hass.data[DOMAIN].cloudhooks.async_delete(webhook_id)
|
2019-03-04 03:03:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
def is_cloudhook_request(request):
|
|
|
|
"""Test if a request came from a cloudhook.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
|
|
|
return isinstance(request, MockRequest)
|
|
|
|
|
|
|
|
|
2018-10-01 06:52:42 +00:00
|
|
|
async def async_setup(hass, config):
|
2017-08-29 20:40:08 +00:00
|
|
|
"""Initialize the Home Assistant cloud."""
|
2019-03-11 19:21:20 +00:00
|
|
|
from hass_nabucasa import Cloud
|
|
|
|
from .client import CloudClient
|
|
|
|
|
|
|
|
# Process configs
|
2017-08-29 20:40:08 +00:00
|
|
|
if DOMAIN in config:
|
2018-01-05 20:33:22 +00:00
|
|
|
kwargs = dict(config[DOMAIN])
|
2017-10-15 02:43:14 +00:00
|
|
|
else:
|
|
|
|
kwargs = {CONF_MODE: DEFAULT_MODE}
|
2017-08-29 20:40:08 +00:00
|
|
|
|
2018-01-05 20:33:22 +00:00
|
|
|
alexa_conf = kwargs.pop(CONF_ALEXA, None) or ALEXA_SCHEMA({})
|
2019-03-11 19:21:20 +00:00
|
|
|
google_conf = kwargs.pop(CONF_GOOGLE_ACTIONS, None) or GACTIONS_SCHEMA({})
|
2018-01-09 23:14:56 +00:00
|
|
|
|
2019-03-11 19:21:20 +00:00
|
|
|
prefs = CloudPreferences(hass)
|
|
|
|
await prefs.async_initialize()
|
2018-01-03 18:16:59 +00:00
|
|
|
|
2019-03-11 19:21:20 +00:00
|
|
|
websession = hass.helpers.aiohttp_client.async_get_clientsession()
|
|
|
|
client = CloudClient(hass, prefs, websession, alexa_conf, google_conf)
|
|
|
|
cloud = hass.data[DOMAIN] = Cloud(client, **kwargs)
|
2018-01-09 23:14:56 +00:00
|
|
|
|
2019-03-11 19:21:20 +00:00
|
|
|
async def _startup(event):
|
|
|
|
"""Startup event."""
|
|
|
|
await cloud.start()
|
|
|
|
|
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _startup)
|
|
|
|
|
|
|
|
async def _shutdown(event):
|
|
|
|
"""Shutdown event."""
|
|
|
|
await cloud.stop()
|
2017-10-15 02:43:14 +00:00
|
|
|
|
2019-03-11 19:21:20 +00:00
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
|
2017-10-15 02:43:14 +00:00
|
|
|
|
2019-03-11 19:21:20 +00:00
|
|
|
async def _service_handler(service):
|
|
|
|
"""Handle service for cloud."""
|
|
|
|
if service.service == SERVICE_REMOTE_CONNECT:
|
|
|
|
await cloud.remote.connect()
|
2019-03-12 14:54:04 +00:00
|
|
|
await prefs.async_update(remote_enabled=True)
|
2019-03-11 19:21:20 +00:00
|
|
|
elif service.service == SERVICE_REMOTE_DISCONNECT:
|
|
|
|
await cloud.remote.disconnect()
|
2019-03-12 14:54:04 +00:00
|
|
|
await prefs.async_update(remote_enabled=False)
|
2019-03-11 19:21:20 +00:00
|
|
|
|
|
|
|
hass.services.async_register(
|
|
|
|
DOMAIN, SERVICE_REMOTE_CONNECT, _service_handler)
|
|
|
|
hass.services.async_register(
|
|
|
|
DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler)
|
|
|
|
|
|
|
|
await http_api.async_setup(hass)
|
|
|
|
return True
|