From f6d511ac1a10707bf6da31e0c372695130e1db2c Mon Sep 17 00:00:00 2001 From: r4nd0mbr1ck <23737685+r4nd0mbr1ck@users.noreply.github.com> Date: Tue, 14 Nov 2017 03:32:23 +1100 Subject: [PATCH] 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 --- .../components/google_assistant/__init__.py | 55 ++++++++++++++++++- .../components/google_assistant/const.py | 6 ++ .../components/google_assistant/http.py | 19 +++++-- .../components/google_assistant/services.yaml | 2 + .../components/google_assistant/test_init.py | 31 +++++++++++ 5 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/google_assistant/services.yaml create mode 100644 tests/components/google_assistant/test_init.py diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 53de8764a12..2db36d8829f 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -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 https://home-assistant.io/components/google_assistant/ """ +import os import asyncio import logging +import aiohttp +import async_timeout + import voluptuous as vol # Typing imports @@ -15,11 +19,16 @@ import voluptuous as vol from homeassistant.core import HomeAssistant # 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.aiohttp_client import async_get_clientsession +from homeassistant.loader import bind_hass from .const import ( 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 .http import GoogleAssistantView @@ -28,6 +37,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['http'] +DEFAULT_AGENT_USER_ID = 'home-assistant' + CONFIG_SCHEMA = vol.Schema( { DOMAIN: { @@ -36,17 +47,57 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_ACCESS_TOKEN): cv.string, vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean, 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) +@bind_hass +def request_sync(hass): + """Request sync.""" + hass.services.call(DOMAIN, SERVICE_REQUEST_SYNC) + + @asyncio.coroutine def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Actions component.""" 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(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 diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 80afad82938..c15f14bccdb 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -13,6 +13,8 @@ CONF_PROJECT_ID = 'project_id' CONF_ACCESS_TOKEN = 'access_token' CONF_CLIENT_ID = 'client_id' CONF_ALIASES = 'aliases' +CONF_AGENT_USER_ID = 'agent_user_id' +CONF_API_KEY = 'api_key' DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSED_DOMAINS = [ @@ -44,3 +46,7 @@ TYPE_LIGHT = PREFIX_TYPES + 'LIGHT' TYPE_SWITCH = PREFIX_TYPES + 'SWITCH' TYPE_SCENE = PREFIX_TYPES + 'SCENE' 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' diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 76b911e051a..1458d695163 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -21,10 +21,16 @@ from homeassistant.core import HomeAssistant # NOQA from homeassistant.helpers.entity import Entity # NOQA from .const import ( - CONF_ACCESS_TOKEN, CONF_EXPOSED_DOMAINS, ATTR_GOOGLE_ASSISTANT, - CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSED_DOMAINS, DEFAULT_EXPOSE_BY_DEFAULT, - GOOGLE_ASSISTANT_API_ENDPOINT) -from .smart_home import query_device, entity_to_device, determine_service + GOOGLE_ASSISTANT_API_ENDPOINT, + CONF_ACCESS_TOKEN, + DEFAULT_EXPOSE_BY_DEFAULT, + 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__) @@ -45,6 +51,7 @@ class GoogleAssistantView(HomeAssistantView): DEFAULT_EXPOSE_BY_DEFAULT) self.exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS) + self.agent_user_id = cfg.get(CONF_AGENT_USER_ID) def is_entity_exposed(self, entity) -> bool: """Determine if an entity should be exposed to Google Assistant.""" @@ -82,7 +89,9 @@ class GoogleAssistantView(HomeAssistantView): devices.append(device) return self.json( - make_actions_response(request_id, {'devices': devices})) + make_actions_response(request_id, + {'agentUserId': self.agent_user_id, + 'devices': devices})) @asyncio.coroutine def handle_query(self, diff --git a/homeassistant/components/google_assistant/services.yaml b/homeassistant/components/google_assistant/services.yaml new file mode 100644 index 00000000000..6019b75bd98 --- /dev/null +++ b/homeassistant/components/google_assistant/services.yaml @@ -0,0 +1,2 @@ +request_sync: + description: Send a request_sync command to Google. \ No newline at end of file diff --git a/tests/components/google_assistant/test_init.py b/tests/components/google_assistant/test_init.py new file mode 100644 index 00000000000..9ced9fc329d --- /dev/null +++ b/tests/components/google_assistant/test_init.py @@ -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