mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 10:47:10 +00:00
Voice command API.AI. First import (#5462)
* Voice command API.AI. First import * Fixes suggested by hound * Fixing comments * Fix pylint and pydocstyle errors * Change how speech is defined Also clean some unused constants, remove card type (not used), define a message when action is not defined and improve the message when action is unknown. * Change how speech is defined Clean some constants. Improve error messages. Delete card type, not used. * Tests for new Api.ai component * Use async_add_job to python compatibility. New test to measure response time * Add async_action option to choose between waiting or not for the action to execute * Travis-ci needs more time * Removed timeout tests * Removed timeout tests * Added apiai to .coveragerc as specified by PR doc
This commit is contained in:
parent
88d9d787a6
commit
14f8bc26d1
@ -129,6 +129,7 @@ omit =
|
|||||||
homeassistant/components/alarm_control_panel/concord232.py
|
homeassistant/components/alarm_control_panel/concord232.py
|
||||||
homeassistant/components/alarm_control_panel/nx584.py
|
homeassistant/components/alarm_control_panel/nx584.py
|
||||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||||
|
homeassistant/components/apiai.py
|
||||||
homeassistant/components/binary_sensor/arest.py
|
homeassistant/components/binary_sensor/arest.py
|
||||||
homeassistant/components/binary_sensor/concord232.py
|
homeassistant/components/binary_sensor/concord232.py
|
||||||
homeassistant/components/binary_sensor/flic.py
|
homeassistant/components/binary_sensor/flic.py
|
||||||
|
172
homeassistant/components/apiai.py
Normal file
172
homeassistant/components/apiai.py
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
"""
|
||||||
|
Support for API.AI webhook.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/apiai/
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import copy
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import PROJECT_NAME, HTTP_BAD_REQUEST
|
||||||
|
from homeassistant.helpers import template, script, config_validation as cv
|
||||||
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
INTENTS_API_ENDPOINT = '/api/apiai'
|
||||||
|
|
||||||
|
CONF_INTENTS = 'intents'
|
||||||
|
CONF_SPEECH = 'speech'
|
||||||
|
CONF_ACTION = 'action'
|
||||||
|
CONF_ASYNC_ACTION = 'async_action'
|
||||||
|
|
||||||
|
DEFAULT_CONF_ASYNC_ACTION = False
|
||||||
|
|
||||||
|
DOMAIN = 'apiai'
|
||||||
|
DEPENDENCIES = ['http']
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: {
|
||||||
|
CONF_INTENTS: {
|
||||||
|
cv.string: {
|
||||||
|
vol.Optional(CONF_SPEECH): cv.template,
|
||||||
|
vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||||
|
vol.Optional(CONF_ASYNC_ACTION,
|
||||||
|
default=DEFAULT_CONF_ASYNC_ACTION): cv.boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
"""Activate API.AI component."""
|
||||||
|
intents = config[DOMAIN].get(CONF_INTENTS, {})
|
||||||
|
|
||||||
|
hass.http.register_view(ApiaiIntentsView(hass, intents))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ApiaiIntentsView(HomeAssistantView):
|
||||||
|
"""Handle API.AI requests."""
|
||||||
|
|
||||||
|
url = INTENTS_API_ENDPOINT
|
||||||
|
name = 'api:apiai'
|
||||||
|
|
||||||
|
def __init__(self, hass, intents):
|
||||||
|
"""Initialize API.AI view."""
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.hass = hass
|
||||||
|
intents = copy.deepcopy(intents)
|
||||||
|
template.attach(hass, intents)
|
||||||
|
|
||||||
|
for name, intent in intents.items():
|
||||||
|
if CONF_ACTION in intent:
|
||||||
|
intent[CONF_ACTION] = script.Script(
|
||||||
|
hass, intent[CONF_ACTION], "Apiai intent {}".format(name))
|
||||||
|
|
||||||
|
self.intents = intents
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def post(self, request):
|
||||||
|
"""Handle API.AI."""
|
||||||
|
data = yield from request.json()
|
||||||
|
|
||||||
|
_LOGGER.debug('Received Apiai request: %s', data)
|
||||||
|
|
||||||
|
req = data.get('result')
|
||||||
|
|
||||||
|
if req is None:
|
||||||
|
_LOGGER.error('Received invalid data from Apiai: %s', data)
|
||||||
|
return self.json_message('Expected result value not received',
|
||||||
|
HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
|
action_incomplete = req['actionIncomplete']
|
||||||
|
|
||||||
|
if action_incomplete:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# use intent to no mix HASS actions with this parameter
|
||||||
|
intent = req.get('action')
|
||||||
|
parameters = req.get('parameters')
|
||||||
|
# contexts = req.get('contexts')
|
||||||
|
response = ApiaiResponse(parameters)
|
||||||
|
|
||||||
|
# Default Welcome Intent
|
||||||
|
# Maybe is better to handle this in api.ai directly?
|
||||||
|
#
|
||||||
|
# if intent == 'input.welcome':
|
||||||
|
# response.add_speech(
|
||||||
|
# "Hello, and welcome to the future. How may I help?")
|
||||||
|
# return self.json(response)
|
||||||
|
|
||||||
|
if intent == "":
|
||||||
|
_LOGGER.warning('Received intent with empty action')
|
||||||
|
response.add_speech(
|
||||||
|
"You have not defined an action in your api.ai intent.")
|
||||||
|
return self.json(response)
|
||||||
|
|
||||||
|
config = self.intents.get(intent)
|
||||||
|
|
||||||
|
if config is None:
|
||||||
|
_LOGGER.warning('Received unknown intent %s', intent)
|
||||||
|
response.add_speech(
|
||||||
|
"Intent '%s' is not yet configured within Home Assistant." %
|
||||||
|
intent)
|
||||||
|
return self.json(response)
|
||||||
|
|
||||||
|
speech = config.get(CONF_SPEECH)
|
||||||
|
action = config.get(CONF_ACTION)
|
||||||
|
async_action = config.get(CONF_ASYNC_ACTION)
|
||||||
|
|
||||||
|
if action is not None:
|
||||||
|
# API.AI expects a response in less than 5s
|
||||||
|
if async_action:
|
||||||
|
# Do not wait for the action to be executed.
|
||||||
|
# Needed if the action will take longer than 5s to execute
|
||||||
|
self.hass.async_add_job(action.async_run(response.parameters))
|
||||||
|
else:
|
||||||
|
# Wait for the action to be executed so we can use results to
|
||||||
|
# render the answer
|
||||||
|
yield from action.async_run(response.parameters)
|
||||||
|
|
||||||
|
# pylint: disable=unsubscriptable-object
|
||||||
|
if speech is not None:
|
||||||
|
response.add_speech(speech)
|
||||||
|
|
||||||
|
return self.json(response)
|
||||||
|
|
||||||
|
|
||||||
|
class ApiaiResponse(object):
|
||||||
|
"""Help generating the response for API.AI."""
|
||||||
|
|
||||||
|
def __init__(self, parameters):
|
||||||
|
"""Initialize the response."""
|
||||||
|
self.speech = None
|
||||||
|
self.parameters = {}
|
||||||
|
# Parameter names replace '.' and '-' for '_'
|
||||||
|
for key, value in parameters.items():
|
||||||
|
underscored_key = key.replace('.', '_').replace('-', '_')
|
||||||
|
self.parameters[underscored_key] = value
|
||||||
|
|
||||||
|
def add_speech(self, text):
|
||||||
|
"""Add speech to the response."""
|
||||||
|
assert self.speech is None
|
||||||
|
|
||||||
|
if isinstance(text, template.Template):
|
||||||
|
text = text.async_render(self.parameters)
|
||||||
|
|
||||||
|
self.speech = text
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
"""Return response in an API.AI valid dict."""
|
||||||
|
return {
|
||||||
|
'speech': self.speech,
|
||||||
|
'displayText': self.speech,
|
||||||
|
'source': PROJECT_NAME,
|
||||||
|
}
|
513
tests/components/test_apiai.py
Normal file
513
tests/components/test_apiai.py
Normal file
@ -0,0 +1,513 @@
|
|||||||
|
"""The tests for the APIAI component."""
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
import json
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant import bootstrap, const
|
||||||
|
from homeassistant.components import apiai, 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, apiai.INTENTS_API_ENDPOINT)
|
||||||
|
|
||||||
|
HA_HEADERS = {
|
||||||
|
const.HTTP_HEADER_HA_AUTH: API_PASSWORD,
|
||||||
|
const.HTTP_HEADER_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://docs.api.ai/docs/webhook
|
||||||
|
|
||||||
|
# An unknown action takes 8s 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()
|
||||||
|
|
||||||
|
bootstrap.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", "apiai", mock_service)
|
||||||
|
|
||||||
|
bootstrap.setup_component(hass, apiai.DOMAIN, {
|
||||||
|
# Key is here to verify we allow other keys in config too
|
||||||
|
"homeassistant": {},
|
||||||
|
"apiai": {
|
||||||
|
"intents": {
|
||||||
|
"WhereAreWeIntent": {
|
||||||
|
"speech":
|
||||||
|
"""
|
||||||
|
{%- 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": "You told us your sign is {{ ZodiacSign }}.",
|
||||||
|
},
|
||||||
|
"CallServiceIntent": {
|
||||||
|
"speech": "Service called",
|
||||||
|
"action": {
|
||||||
|
"service": "test.apiai",
|
||||||
|
"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 TestApiai(unittest.TestCase):
|
||||||
|
"""Test APIAI."""
|
||||||
|
|
||||||
|
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)
|
||||||
|
self.assertEqual(200, req.status_code)
|
||||||
|
self.assertEqual("", req.text)
|
||||||
|
|
||||||
|
def test_intent_slot_filling(self):
|
||||||
|
"""Test when API.AI 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)
|
||||||
|
self.assertEqual(200, req.status_code)
|
||||||
|
self.assertEqual("", 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)
|
||||||
|
self.assertEqual(200, req.status_code)
|
||||||
|
text = req.json().get("speech")
|
||||||
|
self.assertEqual("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)
|
||||||
|
self.assertEqual(200, req.status_code)
|
||||||
|
text = req.json().get("speech")
|
||||||
|
self.assertEqual("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)
|
||||||
|
self.assertEqual(200, req.status_code)
|
||||||
|
text = req.json().get("speech")
|
||||||
|
|
||||||
|
self.assertEqual("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)
|
||||||
|
self.assertEqual(200, req.status_code)
|
||||||
|
text = req.json().get("speech")
|
||||||
|
self.assertEqual("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)
|
||||||
|
self.assertEqual(200, req.status_code)
|
||||||
|
self.assertEqual(call_count + 1, len(calls))
|
||||||
|
call = calls[-1]
|
||||||
|
self.assertEqual("test", call.domain)
|
||||||
|
self.assertEqual("apiai", call.service)
|
||||||
|
self.assertEqual(["switch.test"], call.data.get("entity_id"))
|
||||||
|
self.assertEqual("virgo", call.data.get("hello"))
|
||||||
|
|
||||||
|
def test_intent_with_no_action(self):
|
||||||
|
"""Test a 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)
|
||||||
|
self.assertEqual(200, req.status_code)
|
||||||
|
text = req.json().get("speech")
|
||||||
|
self.assertEqual(
|
||||||
|
"You have not defined an action in your api.ai intent.", text)
|
||||||
|
|
||||||
|
def test_intent_with_unknown_action(self):
|
||||||
|
"""Test a 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)
|
||||||
|
self.assertEqual(200, req.status_code)
|
||||||
|
text = req.json().get("speech")
|
||||||
|
self.assertEqual(
|
||||||
|
"Intent 'unknown' is not yet configured within Home Assistant.",
|
||||||
|
text)
|
Loading…
x
Reference in New Issue
Block a user