Google Assistant request sync service (#10165)

* Initial commit for request_sync functionality

* Fixes for Tox results

* Fixed all tox issues and tested locally with GA

* Review comments - api_key, conditional read descriptions

* Add test for service
This commit is contained in:
r4nd0mbr1ck 2017-11-14 03:32:23 +11:00 committed by Paulus Schoutsen
parent bc23799c71
commit f6d511ac1a
5 changed files with 106 additions and 7 deletions

View file

@ -4,9 +4,13 @@ Support for Actions on Google Assistant Smart Home Control.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/google_assistant/ https://home-assistant.io/components/google_assistant/
""" """
import os
import asyncio import asyncio
import logging import logging
import aiohttp
import async_timeout
import voluptuous as vol import voluptuous as vol
# Typing imports # Typing imports
@ -15,11 +19,16 @@ import voluptuous as vol
from homeassistant.core import HomeAssistant # NOQA from homeassistant.core import HomeAssistant # NOQA
from typing import Dict, Any # NOQA from typing import Dict, Any # NOQA
from homeassistant import config as conf_util
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.loader import bind_hass
from .const import ( from .const import (
DOMAIN, CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN, DOMAIN, CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN,
CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS,
CONF_AGENT_USER_ID, CONF_API_KEY,
SERVICE_REQUEST_SYNC, REQUEST_SYNC_BASE_URL
) )
from .auth import GoogleAssistantAuthView from .auth import GoogleAssistantAuthView
from .http import GoogleAssistantView from .http import GoogleAssistantView
@ -28,6 +37,8 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
DEFAULT_AGENT_USER_ID = 'home-assistant'
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: { DOMAIN: {
@ -36,17 +47,57 @@ CONFIG_SCHEMA = vol.Schema(
vol.Required(CONF_ACCESS_TOKEN): cv.string, vol.Required(CONF_ACCESS_TOKEN): cv.string,
vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean, vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean,
vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list, vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list,
vol.Optional(CONF_AGENT_USER_ID,
default=DEFAULT_AGENT_USER_ID): cv.string,
vol.Optional(CONF_API_KEY): cv.string
} }
}, },
extra=vol.ALLOW_EXTRA) extra=vol.ALLOW_EXTRA)
@bind_hass
def request_sync(hass):
"""Request sync."""
hass.services.call(DOMAIN, SERVICE_REQUEST_SYNC)
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
"""Activate Google Actions component.""" """Activate Google Actions component."""
config = yaml_config.get(DOMAIN, {}) config = yaml_config.get(DOMAIN, {})
agent_user_id = config.get(CONF_AGENT_USER_ID)
api_key = config.get(CONF_API_KEY)
if api_key is not None:
descriptions = yield from hass.async_add_job(
conf_util.load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml')
)
hass.http.register_view(GoogleAssistantAuthView(hass, config)) hass.http.register_view(GoogleAssistantAuthView(hass, config))
hass.http.register_view(GoogleAssistantView(hass, config)) hass.http.register_view(GoogleAssistantView(hass, config))
@asyncio.coroutine
def request_sync_service_handler(call):
"""Handle request sync service calls."""
websession = async_get_clientsession(hass)
try:
with async_timeout.timeout(5, loop=hass.loop):
res = yield from websession.post(
REQUEST_SYNC_BASE_URL,
params={'key': api_key},
json={'agent_user_id': agent_user_id})
_LOGGER.info("Submitted request_sync request to Google")
res.raise_for_status()
except aiohttp.ClientResponseError:
body = yield from res.read()
_LOGGER.error(
'request_sync request failed: %d %s', res.status, body)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Could not contact Google for request_sync")
# Register service only if api key is provided
if api_key is not None:
hass.services.async_register(
DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler,
descriptions.get(SERVICE_REQUEST_SYNC))
return True return True

View file

@ -13,6 +13,8 @@ CONF_PROJECT_ID = 'project_id'
CONF_ACCESS_TOKEN = 'access_token' CONF_ACCESS_TOKEN = 'access_token'
CONF_CLIENT_ID = 'client_id' CONF_CLIENT_ID = 'client_id'
CONF_ALIASES = 'aliases' CONF_ALIASES = 'aliases'
CONF_AGENT_USER_ID = 'agent_user_id'
CONF_API_KEY = 'api_key'
DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSE_BY_DEFAULT = True
DEFAULT_EXPOSED_DOMAINS = [ DEFAULT_EXPOSED_DOMAINS = [
@ -44,3 +46,7 @@ TYPE_LIGHT = PREFIX_TYPES + 'LIGHT'
TYPE_SWITCH = PREFIX_TYPES + 'SWITCH' TYPE_SWITCH = PREFIX_TYPES + 'SWITCH'
TYPE_SCENE = PREFIX_TYPES + 'SCENE' TYPE_SCENE = PREFIX_TYPES + 'SCENE'
TYPE_THERMOSTAT = PREFIX_TYPES + 'THERMOSTAT' TYPE_THERMOSTAT = PREFIX_TYPES + 'THERMOSTAT'
SERVICE_REQUEST_SYNC = 'request_sync'
HOMEGRAPH_URL = 'https://homegraph.googleapis.com/'
REQUEST_SYNC_BASE_URL = HOMEGRAPH_URL + 'v1/devices:requestSync'

View file

@ -21,10 +21,16 @@ from homeassistant.core import HomeAssistant # NOQA
from homeassistant.helpers.entity import Entity # NOQA from homeassistant.helpers.entity import Entity # NOQA
from .const import ( from .const import (
CONF_ACCESS_TOKEN, CONF_EXPOSED_DOMAINS, ATTR_GOOGLE_ASSISTANT, GOOGLE_ASSISTANT_API_ENDPOINT,
CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSED_DOMAINS, DEFAULT_EXPOSE_BY_DEFAULT, CONF_ACCESS_TOKEN,
GOOGLE_ASSISTANT_API_ENDPOINT) DEFAULT_EXPOSE_BY_DEFAULT,
from .smart_home import query_device, entity_to_device, determine_service DEFAULT_EXPOSED_DOMAINS,
CONF_EXPOSE_BY_DEFAULT,
CONF_EXPOSED_DOMAINS,
ATTR_GOOGLE_ASSISTANT,
CONF_AGENT_USER_ID
)
from .smart_home import entity_to_device, query_device, determine_service
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -45,6 +51,7 @@ class GoogleAssistantView(HomeAssistantView):
DEFAULT_EXPOSE_BY_DEFAULT) DEFAULT_EXPOSE_BY_DEFAULT)
self.exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS, self.exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS,
DEFAULT_EXPOSED_DOMAINS) DEFAULT_EXPOSED_DOMAINS)
self.agent_user_id = cfg.get(CONF_AGENT_USER_ID)
def is_entity_exposed(self, entity) -> bool: def is_entity_exposed(self, entity) -> bool:
"""Determine if an entity should be exposed to Google Assistant.""" """Determine if an entity should be exposed to Google Assistant."""
@ -82,7 +89,9 @@ class GoogleAssistantView(HomeAssistantView):
devices.append(device) devices.append(device)
return self.json( return self.json(
make_actions_response(request_id, {'devices': devices})) make_actions_response(request_id,
{'agentUserId': self.agent_user_id,
'devices': devices}))
@asyncio.coroutine @asyncio.coroutine
def handle_query(self, def handle_query(self,

View file

@ -0,0 +1,2 @@
request_sync:
description: Send a request_sync command to Google.

View file

@ -0,0 +1,31 @@
"""The tests for google-assistant init."""
import asyncio
from homeassistant.setup import async_setup_component
from homeassistant.components import google_assistant as ga
GA_API_KEY = "Agdgjsj399sdfkosd932ksd"
GA_AGENT_USER_ID = "testid"
@asyncio.coroutine
def test_request_sync_service(aioclient_mock, hass):
"""Test that it posts to the request_sync url."""
aioclient_mock.post(
ga.const.REQUEST_SYNC_BASE_URL, status=200)
yield from async_setup_component(hass, 'google_assistant', {
'google_assistant': {
'project_id': 'test_project',
'client_id': 'r7328kwdsdfsdf03223409',
'access_token': '8wdsfjsf932492342349234',
'agent_user_id': GA_AGENT_USER_ID,
'api_key': GA_API_KEY
}})
assert aioclient_mock.call_count == 0
yield from hass.services.async_call(ga.const.DOMAIN,
ga.const.SERVICE_REQUEST_SYNC,
blocking=True)
assert aioclient_mock.call_count == 1