From 60080a529df59a0b0c9ff108433ec126a4bef3a0 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sun, 28 Oct 2018 11:25:43 -0700 Subject: [PATCH] Migrate dialogflow over to the new webhook component (#17804) * Migrate dialogflow over to the new webhook component * Updating dialogflow unit tests * Lint * Revert changes to HomeAssistantView * Use json_response from aiohttp --- .../dialogflow/.translations/en.json | 18 + .../{dialogflow.py => dialogflow/__init__.py} | 106 ++-- .../components/dialogflow/strings.json | 18 + homeassistant/config_entries.py | 1 + tests/components/dialogflow/__init__.py | 1 + tests/components/dialogflow/test_init.py | 529 ++++++++++++++++++ tests/components/test_dialogflow.py | 525 ----------------- 7 files changed, 626 insertions(+), 572 deletions(-) create mode 100644 homeassistant/components/dialogflow/.translations/en.json rename homeassistant/components/{dialogflow.py => dialogflow/__init__.py} (58%) create mode 100644 homeassistant/components/dialogflow/strings.json create mode 100644 tests/components/dialogflow/__init__.py create mode 100644 tests/components/dialogflow/test_init.py delete mode 100644 tests/components/test_dialogflow.py diff --git a/homeassistant/components/dialogflow/.translations/en.json b/homeassistant/components/dialogflow/.translations/en.json new file mode 100644 index 00000000000..4a3e91a3e50 --- /dev/null +++ b/homeassistant/components/dialogflow/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Dialogflow", + "step": { + "user": { + "title": "Set up the Dialogflow Webhook", + "description": "Are you sure you want to set up Dialogflow?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary.", + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Dialogflow messages." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup [webhook integration of Dialogflow]({dialogflow_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) for further details." + } + } +} diff --git a/homeassistant/components/dialogflow.py b/homeassistant/components/dialogflow/__init__.py similarity index 58% rename from homeassistant/components/dialogflow.py rename to homeassistant/components/dialogflow/__init__.py index 0f275a7fe66..900dae5c7c1 100644 --- a/homeassistant/components/dialogflow.py +++ b/homeassistant/components/dialogflow/__init__.py @@ -7,24 +7,16 @@ https://home-assistant.io/components/dialogflow/ import logging import voluptuous as vol +from aiohttp import web +from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import intent, template -from homeassistant.components.http import HomeAssistantView - +from homeassistant.helpers import intent, template, config_entry_flow _LOGGER = logging.getLogger(__name__) -CONF_INTENTS = 'intents' -CONF_SPEECH = 'speech' -CONF_ACTION = 'action' -CONF_ASYNC_ACTION = 'async_action' - -DEFAULT_CONF_ASYNC_ACTION = False -DEPENDENCIES = ['http'] +DEPENDENCIES = ['webhook'] DOMAIN = 'dialogflow' -INTENTS_API_ENDPOINT = '/api/dialogflow' - SOURCE = "Home Assistant Dialogflow" CONFIG_SCHEMA = vol.Schema({ @@ -38,52 +30,72 @@ class DialogFlowError(HomeAssistantError): async def async_setup(hass, config): """Set up Dialogflow component.""" - hass.http.register_view(DialogflowIntentsView) - return True -class DialogflowIntentsView(HomeAssistantView): - """Handle Dialogflow requests.""" +async def handle_webhook(hass, webhook_id, request): + """Handle incoming webhook with Dialogflow requests.""" + message = await request.json() - url = INTENTS_API_ENDPOINT - name = 'api:dialogflow' + _LOGGER.debug("Received Dialogflow request: %s", message) - async def post(self, request): - """Handle Dialogflow.""" - hass = request.app['hass'] - message = await request.json() + try: + response = await async_handle_message(hass, message) + return b'' if response is None else web.json_response(response) - _LOGGER.debug("Received Dialogflow request: %s", message) + except DialogFlowError as err: + _LOGGER.warning(str(err)) + return web.json_response( + dialogflow_error_response(message, str(err)) + ) - try: - response = await async_handle_message(hass, message) - return b'' if response is None else self.json(response) + except intent.UnknownIntent as err: + _LOGGER.warning(str(err)) + return web.json_response( + dialogflow_error_response( + message, + "This intent is not yet configured within Home Assistant." + ) + ) - except DialogFlowError as err: - _LOGGER.warning(str(err)) - return self.json(dialogflow_error_response( - hass, message, str(err))) + except intent.InvalidSlotInfo as err: + _LOGGER.warning(str(err)) + return web.json_response( + dialogflow_error_response( + message, + "Invalid slot information received for this intent." + ) + ) - except intent.UnknownIntent as err: - _LOGGER.warning(str(err)) - return self.json(dialogflow_error_response( - hass, message, - "This intent is not yet configured within Home Assistant.")) - - except intent.InvalidSlotInfo as err: - _LOGGER.warning(str(err)) - return self.json(dialogflow_error_response( - hass, message, - "Invalid slot information received for this intent.")) - - except intent.IntentError as err: - _LOGGER.warning(str(err)) - return self.json(dialogflow_error_response( - hass, message, "Error handling intent.")) + except intent.IntentError as err: + _LOGGER.warning(str(err)) + return web.json_response( + dialogflow_error_response(message, "Error handling intent.")) -def dialogflow_error_response(hass, message, error): +async def async_setup_entry(hass, entry): + """Configure based on config entry.""" + hass.components.webhook.async_register( + entry.data[CONF_WEBHOOK_ID], handle_webhook) + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) + return True + +config_entry_flow.register_webhook_flow( + DOMAIN, + 'Dialogflow Webhook', + { + 'dialogflow_url': 'https://dialogflow.com/docs/fulfillment#webhook', + 'docs_url': 'https://www.home-assistant.io/components/dialogflow/' + } +) + + +def dialogflow_error_response(message, error): """Return a response saying the error message.""" dialogflow_response = DialogflowResponse(message['result']['parameters']) dialogflow_response.add_speech(error) diff --git a/homeassistant/components/dialogflow/strings.json b/homeassistant/components/dialogflow/strings.json new file mode 100644 index 00000000000..4a3e91a3e50 --- /dev/null +++ b/homeassistant/components/dialogflow/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Dialogflow", + "step": { + "user": { + "title": "Set up the Dialogflow Webhook", + "description": "Are you sure you want to set up Dialogflow?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary.", + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Dialogflow messages." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup [webhook integration of Dialogflow]({dialogflow_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) for further details." + } + } +} diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index d37bd8cb558..eaef97011fc 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -137,6 +137,7 @@ HANDLERS = Registry() FLOWS = [ 'cast', 'deconz', + 'dialogflow', 'hangouts', 'homematicip_cloud', 'hue', diff --git a/tests/components/dialogflow/__init__.py b/tests/components/dialogflow/__init__.py new file mode 100644 index 00000000000..b9fdf70afb1 --- /dev/null +++ b/tests/components/dialogflow/__init__.py @@ -0,0 +1 @@ +"""Tests for the Dialogflow component.""" diff --git a/tests/components/dialogflow/test_init.py b/tests/components/dialogflow/test_init.py new file mode 100644 index 00000000000..a2ac5b85d07 --- /dev/null +++ b/tests/components/dialogflow/test_init.py @@ -0,0 +1,529 @@ +"""The tests for the Dialogflow component.""" +import json +from unittest.mock import Mock + +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components import dialogflow, intent_script +from homeassistant.core import callback +from homeassistant.setup import async_setup_component + +SESSION_ID = "a9b84cec-46b6-484e-8f31-f65dba03ae6d" +INTENT_ID = "c6a74079-a8f0-46cd-b372-5a934d23591c" +INTENT_NAME = "tests" +REQUEST_ID = "19ef7e78-fe15-4e94-99dd-0c0b1e8753c3" +REQUEST_TIMESTAMP = "2017-01-21T17:54:18.952Z" +CONTEXT_NAME = "78a5db95-b7d6-4d50-9c9b-2fc73a5e34c3_id_dialog_context" + +calls = [] + + +@pytest.fixture +async def fixture(hass, aiohttp_client): + """Initialize a Home Assistant server for testing this module.""" + @callback + def mock_service(call): + """Mock action call.""" + calls.append(call) + + hass.services.async_register('test', 'dialogflow', mock_service) + + await async_setup_component(hass, dialogflow.DOMAIN, { + "dialogflow": {}, + }) + await async_setup_component(hass, intent_script.DOMAIN, { + "intent_script": { + "WhereAreWeIntent": { + "speech": { + "type": "plain", + "text": """ + {%- if is_state("device_tracker.paulus", "home") + and is_state("device_tracker.anne_therese", + "home") -%} + You are both home, you silly + {%- else -%} + Anne Therese is at {{ + states("device_tracker.anne_therese") + }} and Paulus is at {{ + states("device_tracker.paulus") + }} + {% endif %} + """, + } + }, + "GetZodiacHoroscopeIntent": { + "speech": { + "type": "plain", + "text": "You told us your sign is {{ ZodiacSign }}.", + } + }, + "CallServiceIntent": { + "speech": { + "type": "plain", + "text": "Service called", + }, + "action": { + "service": "test.dialogflow", + "data_template": { + "hello": "{{ ZodiacSign }}" + }, + "entity_id": "switch.test", + } + } + } + }) + + hass.config.api = Mock(base_url='http://example.com') + result = await hass.config_entries.flow.async_init( + 'dialogflow', + context={ + 'source': 'user' + } + ) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM, result + + result = await hass.config_entries.flow.async_configure( + result['flow_id'], {}) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + webhook_id = result['result'].data['webhook_id'] + + return await aiohttp_client(hass.http.app), webhook_id + + +async def test_intent_action_incomplete(fixture): + """Test when action is not completed.""" + mock_client, webhook_id = fixture + data = { + "id": REQUEST_ID, + "timestamp": REQUEST_TIMESTAMP, + "result": { + "source": "agent", + "resolvedQuery": "my zodiac sign is virgo", + "speech": "", + "action": "GetZodiacHoroscopeIntent", + "actionIncomplete": True, + "parameters": { + "ZodiacSign": "virgo" + }, + "metadata": { + "intentId": INTENT_ID, + "webhookUsed": "true", + "webhookForSlotFillingUsed": "false", + "intentName": INTENT_NAME + }, + "fulfillment": { + "speech": "", + "messages": [ + { + "type": 0, + "speech": "" + } + ] + }, + "score": 1 + }, + "status": { + "code": 200, + "errorType": "success" + }, + "sessionId": SESSION_ID, + "originalRequest": None + } + + response = await mock_client.post( + '/api/webhook/{}'.format(webhook_id), + data=json.dumps(data) + ) + assert 200 == response.status + assert "" == await response.text() + + +async def test_intent_slot_filling(fixture): + """Test when Dialogflow asks for slot-filling return none.""" + mock_client, webhook_id = fixture + data = { + "id": REQUEST_ID, + "timestamp": REQUEST_TIMESTAMP, + "result": { + "source": "agent", + "resolvedQuery": "my zodiac sign is", + "speech": "", + "action": "GetZodiacHoroscopeIntent", + "actionIncomplete": True, + "parameters": { + "ZodiacSign": "" + }, + "contexts": [ + { + "name": CONTEXT_NAME, + "parameters": { + "ZodiacSign.original": "", + "ZodiacSign": "" + }, + "lifespan": 2 + }, + { + "name": "tests_ha_dialog_context", + "parameters": { + "ZodiacSign.original": "", + "ZodiacSign": "" + }, + "lifespan": 2 + }, + { + "name": "tests_ha_dialog_params_zodiacsign", + "parameters": { + "ZodiacSign.original": "", + "ZodiacSign": "" + }, + "lifespan": 1 + } + ], + "metadata": { + "intentId": INTENT_ID, + "webhookUsed": "true", + "webhookForSlotFillingUsed": "true", + "intentName": INTENT_NAME + }, + "fulfillment": { + "speech": "What is the ZodiacSign?", + "messages": [ + { + "type": 0, + "speech": "What is the ZodiacSign?" + } + ] + }, + "score": 0.77 + }, + "status": { + "code": 200, + "errorType": "success" + }, + "sessionId": SESSION_ID, + "originalRequest": None + } + + response = await mock_client.post( + '/api/webhook/{}'.format(webhook_id), + data=json.dumps(data) + ) + assert 200 == response.status + assert "" == await response.text() + + +async def test_intent_request_with_parameters(fixture): + """Test a request with parameters.""" + mock_client, webhook_id = fixture + data = { + "id": REQUEST_ID, + "timestamp": REQUEST_TIMESTAMP, + "result": { + "source": "agent", + "resolvedQuery": "my zodiac sign is virgo", + "speech": "", + "action": "GetZodiacHoroscopeIntent", + "actionIncomplete": False, + "parameters": { + "ZodiacSign": "virgo" + }, + "contexts": [], + "metadata": { + "intentId": INTENT_ID, + "webhookUsed": "true", + "webhookForSlotFillingUsed": "false", + "intentName": INTENT_NAME + }, + "fulfillment": { + "speech": "", + "messages": [ + { + "type": 0, + "speech": "" + } + ] + }, + "score": 1 + }, + "status": { + "code": 200, + "errorType": "success" + }, + "sessionId": SESSION_ID, + "originalRequest": None + } + response = await mock_client.post( + '/api/webhook/{}'.format(webhook_id), + data=json.dumps(data) + ) + assert 200 == response.status + text = (await response.json()).get("speech") + assert "You told us your sign is virgo." == text + + +async def test_intent_request_with_parameters_but_empty(fixture): + """Test a request with parameters but empty value.""" + mock_client, webhook_id = fixture + data = { + "id": REQUEST_ID, + "timestamp": REQUEST_TIMESTAMP, + "result": { + "source": "agent", + "resolvedQuery": "my zodiac sign is virgo", + "speech": "", + "action": "GetZodiacHoroscopeIntent", + "actionIncomplete": False, + "parameters": { + "ZodiacSign": "" + }, + "contexts": [], + "metadata": { + "intentId": INTENT_ID, + "webhookUsed": "true", + "webhookForSlotFillingUsed": "false", + "intentName": INTENT_NAME + }, + "fulfillment": { + "speech": "", + "messages": [ + { + "type": 0, + "speech": "" + } + ] + }, + "score": 1 + }, + "status": { + "code": 200, + "errorType": "success" + }, + "sessionId": SESSION_ID, + "originalRequest": None + } + response = await mock_client.post( + '/api/webhook/{}'.format(webhook_id), + data=json.dumps(data) + ) + assert 200 == response.status + text = (await response.json()).get("speech") + assert "You told us your sign is ." == text + + +async def test_intent_request_without_slots(hass, fixture): + """Test a request without slots.""" + mock_client, webhook_id = fixture + data = { + "id": REQUEST_ID, + "timestamp": REQUEST_TIMESTAMP, + "result": { + "source": "agent", + "resolvedQuery": "where are we", + "speech": "", + "action": "WhereAreWeIntent", + "actionIncomplete": False, + "parameters": {}, + "contexts": [], + "metadata": { + "intentId": INTENT_ID, + "webhookUsed": "true", + "webhookForSlotFillingUsed": "false", + "intentName": INTENT_NAME + }, + "fulfillment": { + "speech": "", + "messages": [ + { + "type": 0, + "speech": "" + } + ] + }, + "score": 1 + }, + "status": { + "code": 200, + "errorType": "success" + }, + "sessionId": SESSION_ID, + "originalRequest": None + } + response = await mock_client.post( + '/api/webhook/{}'.format(webhook_id), + data=json.dumps(data) + ) + assert 200 == response.status + text = (await response.json()).get("speech") + + assert "Anne Therese is at unknown and Paulus is at unknown" == \ + text + + hass.states.async_set("device_tracker.paulus", "home") + hass.states.async_set("device_tracker.anne_therese", "home") + + response = await mock_client.post( + '/api/webhook/{}'.format(webhook_id), + data=json.dumps(data) + ) + assert 200 == response.status + text = (await response.json()).get("speech") + assert "You are both home, you silly" == text + + +async def test_intent_request_calling_service(fixture): + """Test a request for calling a service. + + If this request is done async the test could finish before the action + has been executed. Hard to test because it will be a race condition. + """ + mock_client, webhook_id = fixture + data = { + "id": REQUEST_ID, + "timestamp": REQUEST_TIMESTAMP, + "result": { + "source": "agent", + "resolvedQuery": "my zodiac sign is virgo", + "speech": "", + "action": "CallServiceIntent", + "actionIncomplete": False, + "parameters": { + "ZodiacSign": "virgo" + }, + "contexts": [], + "metadata": { + "intentId": INTENT_ID, + "webhookUsed": "true", + "webhookForSlotFillingUsed": "false", + "intentName": INTENT_NAME + }, + "fulfillment": { + "speech": "", + "messages": [ + { + "type": 0, + "speech": "" + } + ] + }, + "score": 1 + }, + "status": { + "code": 200, + "errorType": "success" + }, + "sessionId": SESSION_ID, + "originalRequest": None + } + call_count = len(calls) + response = await mock_client.post( + '/api/webhook/{}'.format(webhook_id), + data=json.dumps(data) + ) + assert 200 == response.status + assert call_count + 1 == len(calls) + call = calls[-1] + assert "test" == call.domain + assert "dialogflow" == call.service + assert ["switch.test"] == call.data.get("entity_id") + assert "virgo" == call.data.get("hello") + + +async def test_intent_with_no_action(fixture): + """Test an intent with no defined action.""" + mock_client, webhook_id = fixture + data = { + "id": REQUEST_ID, + "timestamp": REQUEST_TIMESTAMP, + "result": { + "source": "agent", + "resolvedQuery": "my zodiac sign is virgo", + "speech": "", + "action": "", + "actionIncomplete": False, + "parameters": { + "ZodiacSign": "" + }, + "contexts": [], + "metadata": { + "intentId": INTENT_ID, + "webhookUsed": "true", + "webhookForSlotFillingUsed": "false", + "intentName": INTENT_NAME + }, + "fulfillment": { + "speech": "", + "messages": [ + { + "type": 0, + "speech": "" + } + ] + }, + "score": 1 + }, + "status": { + "code": 200, + "errorType": "success" + }, + "sessionId": SESSION_ID, + "originalRequest": None + } + response = await mock_client.post( + '/api/webhook/{}'.format(webhook_id), + data=json.dumps(data) + ) + assert 200 == response.status + text = (await response.json()).get("speech") + assert \ + "You have not defined an action in your Dialogflow intent." == text + + +async def test_intent_with_unknown_action(fixture): + """Test an intent with an action not defined in the conf.""" + mock_client, webhook_id = fixture + data = { + "id": REQUEST_ID, + "timestamp": REQUEST_TIMESTAMP, + "result": { + "source": "agent", + "resolvedQuery": "my zodiac sign is virgo", + "speech": "", + "action": "unknown", + "actionIncomplete": False, + "parameters": { + "ZodiacSign": "" + }, + "contexts": [], + "metadata": { + "intentId": INTENT_ID, + "webhookUsed": "true", + "webhookForSlotFillingUsed": "false", + "intentName": INTENT_NAME + }, + "fulfillment": { + "speech": "", + "messages": [ + { + "type": 0, + "speech": "" + } + ] + }, + "score": 1 + }, + "status": { + "code": 200, + "errorType": "success" + }, + "sessionId": SESSION_ID, + "originalRequest": None + } + response = await mock_client.post( + '/api/webhook/{}'.format(webhook_id), + data=json.dumps(data) + ) + assert 200 == response.status + text = (await response.json()).get("speech") + assert \ + "This intent is not yet configured within Home Assistant." == text diff --git a/tests/components/test_dialogflow.py b/tests/components/test_dialogflow.py deleted file mode 100644 index e05e33b30d6..00000000000 --- a/tests/components/test_dialogflow.py +++ /dev/null @@ -1,525 +0,0 @@ -"""The tests for the Dialogflow component.""" -# pylint: disable=protected-access -import json -import unittest - -import requests -from aiohttp.hdrs import CONTENT_TYPE - -from homeassistant.core import callback -from homeassistant import setup, const -from homeassistant.components import dialogflow, http - -from tests.common import get_test_instance_port, get_test_home_assistant - -API_PASSWORD = 'test1234' -SERVER_PORT = get_test_instance_port() -BASE_API_URL = "http://127.0.0.1:{}".format(SERVER_PORT) -INTENTS_API_URL = "{}{}".format(BASE_API_URL, dialogflow.INTENTS_API_ENDPOINT) - -HA_HEADERS = { - const.HTTP_HEADER_HA_AUTH: API_PASSWORD, - CONTENT_TYPE: const.CONTENT_TYPE_JSON, -} - -SESSION_ID = "a9b84cec-46b6-484e-8f31-f65dba03ae6d" -INTENT_ID = "c6a74079-a8f0-46cd-b372-5a934d23591c" -INTENT_NAME = "tests" -REQUEST_ID = "19ef7e78-fe15-4e94-99dd-0c0b1e8753c3" -REQUEST_TIMESTAMP = "2017-01-21T17:54:18.952Z" -CONTEXT_NAME = "78a5db95-b7d6-4d50-9c9b-2fc73a5e34c3_id_dialog_context" -MAX_RESPONSE_TIME = 5 # https://dialogflow.com/docs/fulfillment - -# An unknown action takes 8 s to return. Request timeout should be bigger to -# allow the test to finish -REQUEST_TIMEOUT = 15 - -# pylint: disable=invalid-name -hass = None -calls = [] - - -# pylint: disable=invalid-name -def setUpModule(): - """Initialize a Home Assistant server for testing this module.""" - global hass - - hass = get_test_home_assistant() - - setup.setup_component( - hass, http.DOMAIN, { - http.DOMAIN: { - http.CONF_API_PASSWORD: API_PASSWORD, - http.CONF_SERVER_PORT: SERVER_PORT, - } - } - ) - - @callback - def mock_service(call): - """Mock action call.""" - calls.append(call) - - hass.services.register('test', 'dialogflow', mock_service) - - assert setup.setup_component(hass, dialogflow.DOMAIN, { - "dialogflow": {}, - }) - assert setup.setup_component(hass, "intent_script", { - "intent_script": { - "WhereAreWeIntent": { - "speech": { - "type": "plain", - "text": """ - {%- if is_state("device_tracker.paulus", "home") - and is_state("device_tracker.anne_therese", - "home") -%} - You are both home, you silly - {%- else -%} - Anne Therese is at {{ - states("device_tracker.anne_therese") - }} and Paulus is at {{ - states("device_tracker.paulus") - }} - {% endif %} - """, - } - }, - "GetZodiacHoroscopeIntent": { - "speech": { - "type": "plain", - "text": "You told us your sign is {{ ZodiacSign }}.", - } - }, - "CallServiceIntent": { - "speech": { - "type": "plain", - "text": "Service called", - }, - "action": { - "service": "test.dialogflow", - "data_template": { - "hello": "{{ ZodiacSign }}" - }, - "entity_id": "switch.test", - } - } - } - }) - - hass.start() - - -# pylint: disable=invalid-name -def tearDownModule(): - """Stop the Home Assistant server.""" - hass.stop() - - -def _intent_req(data): - return requests.post( - INTENTS_API_URL, data=json.dumps(data), timeout=REQUEST_TIMEOUT, - headers=HA_HEADERS) - - -class TestDialogflow(unittest.TestCase): - """Test Dialogflow.""" - - def tearDown(self): - """Stop everything that was started.""" - hass.block_till_done() - - def test_intent_action_incomplete(self): - """Test when action is not completed.""" - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is virgo", - "speech": "", - "action": "GetZodiacHoroscopeIntent", - "actionIncomplete": True, - "parameters": { - "ZodiacSign": "virgo" - }, - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME - }, - "fulfillment": { - "speech": "", - "messages": [ - { - "type": 0, - "speech": "" - } - ] - }, - "score": 1 - }, - "status": { - "code": 200, - "errorType": "success" - }, - "sessionId": SESSION_ID, - "originalRequest": None - } - - req = _intent_req(data) - assert 200 == req.status_code - assert "" == req.text - - def test_intent_slot_filling(self): - """Test when Dialogflow asks for slot-filling return none.""" - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is", - "speech": "", - "action": "GetZodiacHoroscopeIntent", - "actionIncomplete": True, - "parameters": { - "ZodiacSign": "" - }, - "contexts": [ - { - "name": CONTEXT_NAME, - "parameters": { - "ZodiacSign.original": "", - "ZodiacSign": "" - }, - "lifespan": 2 - }, - { - "name": "tests_ha_dialog_context", - "parameters": { - "ZodiacSign.original": "", - "ZodiacSign": "" - }, - "lifespan": 2 - }, - { - "name": "tests_ha_dialog_params_zodiacsign", - "parameters": { - "ZodiacSign.original": "", - "ZodiacSign": "" - }, - "lifespan": 1 - } - ], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "true", - "intentName": INTENT_NAME - }, - "fulfillment": { - "speech": "What is the ZodiacSign?", - "messages": [ - { - "type": 0, - "speech": "What is the ZodiacSign?" - } - ] - }, - "score": 0.77 - }, - "status": { - "code": 200, - "errorType": "success" - }, - "sessionId": SESSION_ID, - "originalRequest": None - } - - req = _intent_req(data) - assert 200 == req.status_code - assert "" == req.text - - def test_intent_request_with_parameters(self): - """Test a request with parameters.""" - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is virgo", - "speech": "", - "action": "GetZodiacHoroscopeIntent", - "actionIncomplete": False, - "parameters": { - "ZodiacSign": "virgo" - }, - "contexts": [], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME - }, - "fulfillment": { - "speech": "", - "messages": [ - { - "type": 0, - "speech": "" - } - ] - }, - "score": 1 - }, - "status": { - "code": 200, - "errorType": "success" - }, - "sessionId": SESSION_ID, - "originalRequest": None - } - req = _intent_req(data) - assert 200 == req.status_code - text = req.json().get("speech") - assert "You told us your sign is virgo." == text - - def test_intent_request_with_parameters_but_empty(self): - """Test a request with parameters but empty value.""" - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is virgo", - "speech": "", - "action": "GetZodiacHoroscopeIntent", - "actionIncomplete": False, - "parameters": { - "ZodiacSign": "" - }, - "contexts": [], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME - }, - "fulfillment": { - "speech": "", - "messages": [ - { - "type": 0, - "speech": "" - } - ] - }, - "score": 1 - }, - "status": { - "code": 200, - "errorType": "success" - }, - "sessionId": SESSION_ID, - "originalRequest": None - } - req = _intent_req(data) - assert 200 == req.status_code - text = req.json().get("speech") - assert "You told us your sign is ." == text - - def test_intent_request_without_slots(self): - """Test a request without slots.""" - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "where are we", - "speech": "", - "action": "WhereAreWeIntent", - "actionIncomplete": False, - "parameters": {}, - "contexts": [], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME - }, - "fulfillment": { - "speech": "", - "messages": [ - { - "type": 0, - "speech": "" - } - ] - }, - "score": 1 - }, - "status": { - "code": 200, - "errorType": "success" - }, - "sessionId": SESSION_ID, - "originalRequest": None - } - req = _intent_req(data) - assert 200 == req.status_code - text = req.json().get("speech") - - assert "Anne Therese is at unknown and Paulus is at unknown" == \ - text - - hass.states.set("device_tracker.paulus", "home") - hass.states.set("device_tracker.anne_therese", "home") - - req = _intent_req(data) - assert 200 == req.status_code - text = req.json().get("speech") - assert "You are both home, you silly" == text - - def test_intent_request_calling_service(self): - """Test a request for calling a service. - - If this request is done async the test could finish before the action - has been executed. Hard to test because it will be a race condition. - """ - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is virgo", - "speech": "", - "action": "CallServiceIntent", - "actionIncomplete": False, - "parameters": { - "ZodiacSign": "virgo" - }, - "contexts": [], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME - }, - "fulfillment": { - "speech": "", - "messages": [ - { - "type": 0, - "speech": "" - } - ] - }, - "score": 1 - }, - "status": { - "code": 200, - "errorType": "success" - }, - "sessionId": SESSION_ID, - "originalRequest": None - } - call_count = len(calls) - req = _intent_req(data) - assert 200 == req.status_code - assert call_count + 1 == len(calls) - call = calls[-1] - assert "test" == call.domain - assert "dialogflow" == call.service - assert ["switch.test"] == call.data.get("entity_id") - assert "virgo" == call.data.get("hello") - - def test_intent_with_no_action(self): - """Test an intent with no defined action.""" - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is virgo", - "speech": "", - "action": "", - "actionIncomplete": False, - "parameters": { - "ZodiacSign": "" - }, - "contexts": [], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME - }, - "fulfillment": { - "speech": "", - "messages": [ - { - "type": 0, - "speech": "" - } - ] - }, - "score": 1 - }, - "status": { - "code": 200, - "errorType": "success" - }, - "sessionId": SESSION_ID, - "originalRequest": None - } - req = _intent_req(data) - assert 200 == req.status_code - text = req.json().get("speech") - assert \ - "You have not defined an action in your Dialogflow intent." == text - - def test_intent_with_unknown_action(self): - """Test an intent with an action not defined in the conf.""" - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is virgo", - "speech": "", - "action": "unknown", - "actionIncomplete": False, - "parameters": { - "ZodiacSign": "" - }, - "contexts": [], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME - }, - "fulfillment": { - "speech": "", - "messages": [ - { - "type": 0, - "speech": "" - } - ] - }, - "score": 1 - }, - "status": { - "code": 200, - "errorType": "success" - }, - "sessionId": SESSION_ID, - "originalRequest": None - } - req = _intent_req(data) - assert 200 == req.status_code - text = req.json().get("speech") - assert \ - "This intent is not yet configured within Home Assistant." == text