Add Intent component (#8434)

* Add intent component

* Add intent script component

* Add shopping list component

* Convert Snips to use intent component

* Convert Alexa to use intent component

* Lint

* Fix Alexa tests

* Update snips test

* Add intent support to conversation

* Add API to view shopping list contents

* Lint

* Fix demo test

* Lint

* lint

* Remove type from slot schema

* Add dependency to conversation

* Move intent to be a helper

* Fix conversation

* Clean up intent helper

* Fix Alexa

* Snips to use new hass.components

* Allow registering intents with conversation at any point in time

* Shopping list to register sentences

* Add HTTP endpoint to Conversation

* Add async action option to intent_script

* Update API.ai to use intents

* Cleanup Alexa

* Shopping list component to register built-in panel

* Rename shopping list intent to inlude Hass name
This commit is contained in:
Paulus Schoutsen 2017-07-21 21:38:53 -07:00 committed by GitHub
parent 7bea69ce83
commit 7edf14e55f
16 changed files with 970 additions and 396 deletions

View file

@ -15,8 +15,8 @@ import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.helpers import template, script, config_validation as cv
from homeassistant.components.http import HomeAssistantView
from homeassistant.helpers import intent, template, config_validation as cv
from homeassistant.components import http
_LOGGER = logging.getLogger(__name__)
@ -60,6 +60,12 @@ class SpeechType(enum.Enum):
ssml = "SSML"
SPEECH_MAPPINGS = {
'plain': SpeechType.plaintext,
'ssml': SpeechType.ssml,
}
class CardType(enum.Enum):
"""The Alexa card types."""
@ -69,20 +75,6 @@ class CardType(enum.Enum):
CONFIG_SCHEMA = vol.Schema({
DOMAIN: {
CONF_INTENTS: {
cv.string: {
vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_CARD): {
vol.Required(CONF_TYPE): cv.enum(CardType),
vol.Required(CONF_TITLE): cv.template,
vol.Required(CONF_CONTENT): cv.template,
},
vol.Optional(CONF_SPEECH): {
vol.Required(CONF_TYPE): cv.enum(SpeechType),
vol.Required(CONF_TEXT): cv.template,
}
}
},
CONF_FLASH_BRIEFINGS: {
cv.string: vol.All(cv.ensure_list, [{
vol.Required(CONF_UID, default=str(uuid.uuid4())): cv.string,
@ -96,40 +88,27 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
@asyncio.coroutine
def async_setup(hass, config):
"""Activate Alexa component."""
intents = config[DOMAIN].get(CONF_INTENTS, {})
flash_briefings = config[DOMAIN].get(CONF_FLASH_BRIEFINGS, {})
hass.http.register_view(AlexaIntentsView(hass, intents))
hass.http.register_view(AlexaIntentsView)
hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefings))
return True
class AlexaIntentsView(HomeAssistantView):
class AlexaIntentsView(http.HomeAssistantView):
"""Handle Alexa requests."""
url = INTENTS_API_ENDPOINT
name = 'api:alexa'
def __init__(self, hass, intents):
"""Initialize Alexa view."""
super().__init__()
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], "Alexa intent {}".format(name))
self.intents = intents
@asyncio.coroutine
def post(self, request):
"""Handle Alexa."""
hass = request.app['hass']
data = yield from request.json()
_LOGGER.debug('Received Alexa request: %s', data)
@ -146,14 +125,14 @@ class AlexaIntentsView(HomeAssistantView):
if req_type == 'SessionEndedRequest':
return None
intent = req.get('intent')
response = AlexaResponse(request.app['hass'], intent)
alexa_intent_info = req.get('intent')
alexa_response = AlexaResponse(hass, alexa_intent_info)
if req_type == 'LaunchRequest':
response.add_speech(
alexa_response.add_speech(
SpeechType.plaintext,
"Hello, and welcome to the future. How may I help?")
return self.json(response)
return self.json(alexa_response)
if req_type != 'IntentRequest':
_LOGGER.warning('Received unsupported request: %s', req_type)
@ -161,38 +140,47 @@ class AlexaIntentsView(HomeAssistantView):
'Received unsupported request: {}'.format(req_type),
HTTP_BAD_REQUEST)
intent_name = intent['name']
config = self.intents.get(intent_name)
intent_name = alexa_intent_info['name']
if config is None:
try:
intent_response = yield from intent.async_handle(
hass, DOMAIN, intent_name,
{key: {'value': value} for key, value
in alexa_response.variables.items()})
except intent.UnknownIntent as err:
_LOGGER.warning('Received unknown intent %s', intent_name)
response.add_speech(
alexa_response.add_speech(
SpeechType.plaintext,
"This intent is not yet configured within Home Assistant.")
return self.json(response)
return self.json(alexa_response)
speech = config.get(CONF_SPEECH)
card = config.get(CONF_CARD)
action = config.get(CONF_ACTION)
except intent.InvalidSlotInfo as err:
_LOGGER.error('Received invalid slot data from Alexa: %s', err)
return self.json_message('Invalid slot data received',
HTTP_BAD_REQUEST)
except intent.IntentError:
_LOGGER.exception('Error handling request for %s', intent_name)
return self.json_message('Error handling intent', HTTP_BAD_REQUEST)
if action is not None:
yield from action.async_run(response.variables)
for intent_speech, alexa_speech in SPEECH_MAPPINGS.items():
if intent_speech in intent_response.speech:
alexa_response.add_speech(
alexa_speech,
intent_response.speech[intent_speech]['speech'])
break
# pylint: disable=unsubscriptable-object
if speech is not None:
response.add_speech(speech[CONF_TYPE], speech[CONF_TEXT])
if 'simple' in intent_response.card:
alexa_response.add_card(
'simple', intent_response.card['simple']['title'],
intent_response.card['simple']['content'])
if card is not None:
response.add_card(card[CONF_TYPE], card[CONF_TITLE],
card[CONF_CONTENT])
return self.json(response)
return self.json(alexa_response)
class AlexaResponse(object):
"""Help generating the response for Alexa."""
def __init__(self, hass, intent=None):
def __init__(self, hass, intent_info):
"""Initialize the response."""
self.hass = hass
self.speech = None
@ -201,8 +189,9 @@ class AlexaResponse(object):
self.session_attributes = {}
self.should_end_session = True
self.variables = {}
if intent is not None and 'slots' in intent:
for key, value in intent['slots'].items():
# Intent is None if request was a LaunchRequest or SessionEndedRequest
if intent_info is not None:
for key, value in intent_info.get('slots', {}).items():
if 'value' in value:
underscored_key = key.replace('.', '_')
self.variables[underscored_key] = value['value']
@ -272,7 +261,7 @@ class AlexaResponse(object):
}
class AlexaFlashBriefingView(HomeAssistantView):
class AlexaFlashBriefingView(http.HomeAssistantView):
"""Handle Alexa Flash Briefing skill requests."""
url = FLASH_BRIEFINGS_API_ENDPOINT

View file

@ -5,13 +5,12 @@ 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.helpers import intent, template
from homeassistant.components.http import HomeAssistantView
_LOGGER = logging.getLogger(__name__)
@ -29,24 +28,14 @@ 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
}
}
}
DOMAIN: {}
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
@asyncio.coroutine
def async_setup(hass, config):
"""Activate API.AI component."""
intents = config[DOMAIN].get(CONF_INTENTS, {})
hass.http.register_view(ApiaiIntentsView(hass, intents))
hass.http.register_view(ApiaiIntentsView)
return True
@ -57,24 +46,10 @@ class ApiaiIntentsView(HomeAssistantView):
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."""
hass = request.app['hass']
data = yield from request.json()
_LOGGER.debug("Received api.ai request: %s", data)
@ -91,55 +66,41 @@ class ApiaiIntentsView(HomeAssistantView):
if action_incomplete:
return None
# use intent to no mix HASS actions with this parameter
intent = req.get('action')
action = req.get('action')
parameters = req.get('parameters')
# contexts = req.get('contexts')
response = ApiaiResponse(parameters)
apiai_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 == "":
if action == "":
_LOGGER.warning("Received intent with empty action")
response.add_speech(
apiai_response.add_speech(
"You have not defined an action in your api.ai intent.")
return self.json(response)
return self.json(apiai_response)
config = self.intents.get(intent)
try:
intent_response = yield from intent.async_handle(
hass, DOMAIN, action,
{key: {'value': value} for key, value
in parameters.items()})
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)
except intent.UnknownIntent as err:
_LOGGER.warning('Received unknown intent %s', action)
apiai_response.add_speech(
"This intent is not yet configured within Home Assistant.")
return self.json(apiai_response)
speech = config.get(CONF_SPEECH)
action = config.get(CONF_ACTION)
async_action = config.get(CONF_ASYNC_ACTION)
except intent.InvalidSlotInfo as err:
_LOGGER.error('Received invalid slot data: %s', err)
return self.json_message('Invalid slot data received',
HTTP_BAD_REQUEST)
except intent.IntentError:
_LOGGER.exception('Error handling request for %s', action)
return self.json_message('Error handling intent', HTTP_BAD_REQUEST)
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)
if 'plain' in intent_response.speech:
apiai_response.add_speech(
intent_response.speech['plain']['speech'])
# pylint: disable=unsubscriptable-object
if speech is not None:
response.add_speech(speech)
return self.json(response)
return self.json(apiai_response)
class ApiaiResponse(object):

View file

@ -4,6 +4,7 @@ Support for functionality to have conversations with Home Assistant.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/conversation/
"""
import asyncio
import logging
import re
import warnings
@ -11,16 +12,17 @@ import warnings
import voluptuous as vol
from homeassistant import core
from homeassistant.loader import bind_hass
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import script
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, HTTP_BAD_REQUEST)
from homeassistant.helpers import intent, config_validation as cv
from homeassistant.components import http
REQUIREMENTS = ['fuzzywuzzy==0.15.0']
DEPENDENCIES = ['http']
ATTR_TEXT = 'text'
ATTR_SENTENCE = 'sentence'
DOMAIN = 'conversation'
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
@ -28,79 +30,168 @@ REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
SERVICE_PROCESS = 'process'
SERVICE_PROCESS_SCHEMA = vol.Schema({
vol.Required(ATTR_TEXT): vol.All(cv.string, vol.Lower),
vol.Required(ATTR_TEXT): cv.string,
})
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({
cv.string: vol.Schema({
vol.Required(ATTR_SENTENCE): cv.string,
vol.Required('action'): cv.SCRIPT_SCHEMA,
vol.Optional('intents'): vol.Schema({
cv.string: vol.All(cv.ensure_list, [cv.string])
})
})}, extra=vol.ALLOW_EXTRA)
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
@core.callback
@bind_hass
def async_register(hass, intent_type, utterances):
"""Register an intent.
Registrations don't require conversations to be loaded. They will become
active once the conversation component is loaded.
"""
intents = hass.data.get(DOMAIN)
if intents is None:
intents = hass.data[DOMAIN] = {}
conf = intents.get(intent_type)
if conf is None:
conf = intents[intent_type] = []
conf.extend(_create_matcher(utterance) for utterance in utterances)
@asyncio.coroutine
def async_setup(hass, config):
"""Register the process service."""
warnings.filterwarnings('ignore', module='fuzzywuzzy')
from fuzzywuzzy import process as fuzzyExtract
logger = logging.getLogger(__name__)
config = config.get(DOMAIN, {})
intents = hass.data.get(DOMAIN)
choices = {attrs[ATTR_SENTENCE]: script.Script(
hass,
attrs['action'],
name)
for name, attrs in config.items()}
if intents is None:
intents = hass.data[DOMAIN] = {}
for intent_type, utterances in config.get('intents', {}).items():
conf = intents.get(intent_type)
if conf is None:
conf = intents[intent_type] = []
conf.extend(_create_matcher(utterance) for utterance in utterances)
@asyncio.coroutine
def process(service):
"""Parse text into commands."""
# if actually configured
if choices:
text = service.data[ATTR_TEXT]
match = fuzzyExtract.extractOne(text, choices.keys())
scorelimit = 60 # arbitrary value
logging.info(
'matched up text %s and found %s',
text,
[match[0] if match[1] > scorelimit else 'nothing']
)
if match[1] > scorelimit:
choices[match[0]].run() # run respective script
return
text = service.data[ATTR_TEXT]
match = REGEX_TURN_COMMAND.match(text)
yield from _process(hass, text)
if not match:
logger.error("Unable to process: %s", text)
return
name, command = match.groups()
entities = {state.entity_id: state.name for state in hass.states.all()}
entity_ids = fuzzyExtract.extractOne(
name, entities, score_cutoff=65)[2]
if not entity_ids:
logger.error(
"Could not find entity id %s from text %s", name, text)
return
if command == 'on':
hass.services.call(core.DOMAIN, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity_ids,
}, blocking=True)
elif command == 'off':
hass.services.call(core.DOMAIN, SERVICE_TURN_OFF, {
ATTR_ENTITY_ID: entity_ids,
}, blocking=True)
else:
logger.error('Got unsupported command %s from text %s',
command, text)
hass.services.register(
hass.services.async_register(
DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA)
hass.http.register_view(ConversationProcessView)
return True
def _create_matcher(utterance):
"""Create a regex that matches the utterance."""
parts = re.split(r'({\w+})', utterance)
group_matcher = re.compile(r'{(\w+)}')
pattern = ['^']
for part in parts:
match = group_matcher.match(part)
if match is None:
pattern.append(part)
continue
pattern.append('(?P<{}>{})'.format(match.groups()[0], r'[\w ]+'))
pattern.append('$')
return re.compile(''.join(pattern), re.I)
@asyncio.coroutine
def _process(hass, text):
"""Process a line of text."""
intents = hass.data.get(DOMAIN, {})
for intent_type, matchers in intents.items():
for matcher in matchers:
match = matcher.match(text)
if not match:
continue
response = yield from intent.async_handle(
hass, DOMAIN, intent_type,
{key: {'value': value} for key, value
in match.groupdict().items()}, text)
return response
from fuzzywuzzy import process as fuzzyExtract
text = text.lower()
match = REGEX_TURN_COMMAND.match(text)
if not match:
_LOGGER.error("Unable to process: %s", text)
return None
name, command = match.groups()
entities = {state.entity_id: state.name for state
in hass.states.async_all()}
entity_ids = fuzzyExtract.extractOne(
name, entities, score_cutoff=65)[2]
if not entity_ids:
_LOGGER.error(
"Could not find entity id %s from text %s", name, text)
return
if command == 'on':
yield from hass.services.async_call(
core.DOMAIN, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity_ids,
}, blocking=True)
elif command == 'off':
yield from hass.services.async_call(
core.DOMAIN, SERVICE_TURN_OFF, {
ATTR_ENTITY_ID: entity_ids,
}, blocking=True)
else:
_LOGGER.error('Got unsupported command %s from text %s',
command, text)
class ConversationProcessView(http.HomeAssistantView):
"""View to retrieve shopping list content."""
url = '/api/conversation/process'
name = "api:conversation:process"
@asyncio.coroutine
def post(self, request):
"""Send a request for processing."""
hass = request.app['hass']
try:
data = yield from request.json()
except ValueError:
return self.json_message('Invalid JSON specified',
HTTP_BAD_REQUEST)
text = data.get('text')
if text is None:
return self.json_message('Missing "text" key in JSON.',
HTTP_BAD_REQUEST)
intent_result = yield from _process(hass, text)
return self.json(intent_result)

View file

@ -12,6 +12,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.config import find_config_file, load_yaml_config_file
from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback
from homeassistant.loader import bind_hass
from homeassistant.components import api
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.auth import is_trusted_ip
@ -75,6 +76,7 @@ SERVICE_SET_THEME_SCHEMA = vol.Schema({
})
@bind_hass
def register_built_in_panel(hass, component_name, sidebar_title=None,
sidebar_icon=None, url_path=None, config=None):
"""Register a built-in panel."""
@ -96,6 +98,7 @@ def register_built_in_panel(hass, component_name, sidebar_title=None,
sidebar_icon, url_path, url, config)
@bind_hass
def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
sidebar_icon=None, url_path=None, url=None, config=None):
"""Register a panel for the frontend.

View file

@ -0,0 +1,100 @@
"""Handle intents with scripts."""
import asyncio
import copy
import logging
import voluptuous as vol
from homeassistant.helpers import (
intent, template, script, config_validation as cv)
DOMAIN = 'intent_script'
CONF_INTENTS = 'intents'
CONF_SPEECH = 'speech'
CONF_ACTION = 'action'
CONF_CARD = 'card'
CONF_TYPE = 'type'
CONF_TITLE = 'title'
CONF_CONTENT = 'content'
CONF_TEXT = 'text'
CONF_ASYNC_ACTION = 'async_action'
DEFAULT_CONF_ASYNC_ACTION = False
CONFIG_SCHEMA = vol.Schema({
DOMAIN: {
cv.string: {
vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ASYNC_ACTION,
default=DEFAULT_CONF_ASYNC_ACTION): cv.boolean,
vol.Optional(CONF_CARD): {
vol.Optional(CONF_TYPE, default='simple'): cv.string,
vol.Required(CONF_TITLE): cv.template,
vol.Required(CONF_CONTENT): cv.template,
},
vol.Optional(CONF_SPEECH): {
vol.Optional(CONF_TYPE, default='plain'): cv.string,
vol.Required(CONF_TEXT): cv.template,
}
}
}
}, extra=vol.ALLOW_EXTRA)
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
def async_setup(hass, config):
"""Activate Alexa component."""
intents = copy.deepcopy(config[DOMAIN])
template.attach(hass, intents)
for intent_type, conf in intents.items():
if CONF_ACTION in conf:
conf[CONF_ACTION] = script.Script(
hass, conf[CONF_ACTION],
"Intent Script {}".format(intent_type))
intent.async_register(hass, ScriptIntentHandler(intent_type, conf))
return True
class ScriptIntentHandler(intent.IntentHandler):
"""Respond to an intent with a script."""
def __init__(self, intent_type, config):
"""Initialize the script intent handler."""
self.intent_type = intent_type
self.config = config
@asyncio.coroutine
def async_handle(self, intent_obj):
"""Handle the intent."""
speech = self.config.get(CONF_SPEECH)
card = self.config.get(CONF_CARD)
action = self.config.get(CONF_ACTION)
is_async_action = self.config.get(CONF_ASYNC_ACTION)
slots = {key: value['value'] for key, value
in intent_obj.slots.items()}
if action is not None:
if is_async_action:
intent_obj.hass.async_add_job(action.async_run(slots))
else:
yield from action.async_run(slots)
response = intent_obj.create_response()
if speech is not None:
response.async_set_speech(speech[CONF_TEXT].async_render(slots),
speech[CONF_TYPE])
if card is not None:
response.async_set_card(
card[CONF_TITLE].async_render(slots),
card[CONF_CONTENT].async_render(slots),
card[CONF_TYPE])
return response

View file

@ -0,0 +1,90 @@
"""Component to manage a shoppling list."""
import asyncio
import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components import http
from homeassistant.helpers import intent
import homeassistant.helpers.config_validation as cv
DOMAIN = 'shopping_list'
DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({DOMAIN: {}}, extra=vol.ALLOW_EXTRA)
EVENT = 'shopping_list_updated'
INTENT_ADD_ITEM = 'HassShoppingListAddItem'
INTENT_LAST_ITEMS = 'HassShoppingListLastItems'
@asyncio.coroutine
def async_setup(hass, config):
"""Initialize the shopping list."""
hass.data[DOMAIN] = []
intent.async_register(hass, AddItemIntent())
intent.async_register(hass, ListTopItemsIntent())
hass.http.register_view(ShoppingListView)
hass.components.conversation.async_register(INTENT_ADD_ITEM, [
'Add {item} to my shopping list',
])
hass.components.conversation.async_register(INTENT_LAST_ITEMS, [
'What is on my shopping list'
])
hass.components.frontend.register_built_in_panel(
'shopping-list', 'Shopping List', 'mdi:cart')
return True
class AddItemIntent(intent.IntentHandler):
"""Handle AddItem intents."""
intent_type = INTENT_ADD_ITEM
slot_schema = {
'item': cv.string
}
@asyncio.coroutine
def async_handle(self, intent_obj):
"""Handle the intent."""
slots = self.async_validate_slots(intent_obj.slots)
item = slots['item']['value']
intent_obj.hass.data[DOMAIN].append(item)
response = intent_obj.create_response()
response.async_set_speech(
"I've added {} to your shopping list".format(item))
intent_obj.hass.bus.async_fire(EVENT)
return response
class ListTopItemsIntent(intent.IntentHandler):
"""Handle AddItem intents."""
intent_type = INTENT_LAST_ITEMS
slot_schema = {
'item': cv.string
}
@asyncio.coroutine
def async_handle(self, intent_obj):
"""Handle the intent."""
response = intent_obj.create_response()
response.async_set_speech(
"These are the top 5 items in your shopping list: {}".format(
', '.join(reversed(intent_obj.hass.data[DOMAIN][-5:]))))
intent_obj.hass.bus.async_fire(EVENT)
return response
class ShoppingListView(http.HomeAssistantView):
"""View to retrieve shopping list content."""
url = '/api/shopping_list'
name = "api:shopping_list"
@callback
def get(self, request):
"""Retrieve if API is running."""
return self.json(request.app['hass'].data[DOMAIN])

View file

@ -5,12 +5,10 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/snips/
"""
import asyncio
import copy
import json
import logging
import voluptuous as vol
from homeassistant.helpers import template, script, config_validation as cv
import homeassistant.loader as loader
from homeassistant.helpers import intent, config_validation as cv
DOMAIN = 'snips'
DEPENDENCIES = ['mqtt']
@ -19,16 +17,10 @@ CONF_ACTION = 'action'
INTENT_TOPIC = 'hermes/nlu/intentParsed'
LOGGER = logging.getLogger(__name__)
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: {
CONF_INTENTS: {
cv.string: {
vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA,
}
}
}
DOMAIN: {}
}, extra=vol.ALLOW_EXTRA)
INTENT_SCHEMA = vol.Schema({
@ -49,74 +41,34 @@ INTENT_SCHEMA = vol.Schema({
@asyncio.coroutine
def async_setup(hass, config):
"""Activate Snips component."""
mqtt = loader.get_component('mqtt')
intents = config[DOMAIN].get(CONF_INTENTS, {})
handler = IntentHandler(hass, intents)
@asyncio.coroutine
def message_received(topic, payload, qos):
"""Handle new messages on MQTT."""
LOGGER.debug("New intent: %s", payload)
yield from handler.handle_intent(payload)
_LOGGER.debug("New intent: %s", payload)
yield from mqtt.async_subscribe(hass, INTENT_TOPIC, message_received)
try:
request = json.loads(payload)
except TypeError:
_LOGGER.error('Received invalid JSON: %s', payload)
return
try:
request = INTENT_SCHEMA(request)
except vol.Invalid as err:
_LOGGER.error('Intent has invalid schema: %s. %s', err, request)
return
intent_type = request['intent']['intentName'].split('__')[-1]
slots = {slot['slotName']: {'value': slot['value']['value']}
for slot in request.get('slots', [])}
try:
yield from intent.async_handle(
hass, DOMAIN, intent_type, slots, request['input'])
except intent.IntentError:
_LOGGER.exception("Error while handling intent.")
yield from hass.components.mqtt.async_subscribe(
INTENT_TOPIC, message_received)
return True
class IntentHandler(object):
"""Help handling intents."""
def __init__(self, hass, intents):
"""Initialize the intent handler."""
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], "Snips intent {}".format(name))
self.intents = intents
@asyncio.coroutine
def handle_intent(self, payload):
"""Handle an intent."""
try:
response = json.loads(payload)
except TypeError:
LOGGER.error('Received invalid JSON: %s', payload)
return
try:
response = INTENT_SCHEMA(response)
except vol.Invalid as err:
LOGGER.error('Intent has invalid schema: %s. %s', err, response)
return
intent = response['intent']['intentName'].split('__')[-1]
config = self.intents.get(intent)
if config is None:
LOGGER.warning("Received unknown intent %s. %s", intent, response)
return
action = config.get(CONF_ACTION)
if action is not None:
slots = self.parse_slots(response)
yield from action.async_run(slots)
# pylint: disable=no-self-use
def parse_slots(self, response):
"""Parse the intent slots."""
parameters = {}
for slot in response.get('slots', []):
key = slot['slotName']
value = slot['value']['value']
if value is not None:
parameters[key] = value
return parameters

View file

@ -0,0 +1,165 @@
"""Module to coordinate user intentions."""
import asyncio
import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
DATA_KEY = 'intent'
_LOGGER = logging.getLogger(__name__)
SLOT_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
SPEECH_TYPE_PLAIN = 'plain'
SPEECH_TYPE_SSML = 'ssml'
@callback
def async_register(hass, handler):
"""Register an intent with Home Assistant."""
intents = hass.data.get(DATA_KEY)
if intents is None:
intents = hass.data[DATA_KEY] = {}
if handler.intent_type in intents:
_LOGGER.warning('Intent %s is being overwritten by %s.',
handler.intent_type, handler)
intents[handler.intent_type] = handler
@asyncio.coroutine
def async_handle(hass, platform, intent_type, slots=None, text_input=None):
"""Handle an intent."""
handler = hass.data.get(DATA_KEY, {}).get(intent_type)
if handler is None:
raise UnknownIntent()
intent = Intent(hass, platform, intent_type, slots or {}, text_input)
try:
_LOGGER.info("Triggering intent handler %s", handler)
result = yield from handler.async_handle(intent)
return result
except vol.Invalid as err:
raise InvalidSlotInfo from err
except Exception as err:
raise IntentHandleError from err
class IntentError(HomeAssistantError):
"""Base class for intent related errors."""
pass
class UnknownIntent(IntentError):
"""When the intent is not registered."""
pass
class InvalidSlotInfo(IntentError):
"""When the slot data is invalid."""
pass
class IntentHandleError(IntentError):
"""Error while handling intent."""
pass
class IntentHandler:
"""Intent handler registration."""
intent_type = None
slot_schema = None
_slot_schema = None
platforms = None
@callback
def async_can_handle(self, intent_obj):
"""Test if an intent can be handled."""
return self.platforms is None or intent_obj.platform in self.platforms
@callback
def async_validate_slots(self, slots):
"""Validate slot information."""
if self.slot_schema is None:
return slots
if self._slot_schema is None:
self._slot_schema = vol.Schema({
key: SLOT_SCHEMA.extend({'value': validator})
for key, validator in self.slot_schema.items()})
return self._slot_schema(slots)
@asyncio.coroutine
def async_handle(self, intent_obj):
"""Handle the intent."""
raise NotImplementedError()
def __repr__(self):
"""String representation of intent handler."""
return '<{} - {}>'.format(self.__class__.__name__, self.intent_type)
class Intent:
"""Hold the intent."""
__slots__ = ['hass', 'platform', 'intent_type', 'slots', 'text_input']
def __init__(self, hass, platform, intent_type, slots, text_input):
"""Initialize an intent."""
self.hass = hass
self.platform = platform
self.intent_type = intent_type
self.slots = slots
self.text_input = text_input
@callback
def create_response(self):
"""Create a response."""
return IntentResponse(self)
class IntentResponse:
"""Response to an intent."""
def __init__(self, intent):
"""Initialize an IntentResponse."""
self.intent = intent
self.speech = {}
self.card = {}
@callback
def async_set_speech(self, speech, speech_type='plain', extra_data=None):
"""Set speech response."""
self.speech[speech_type] = {
'speech': speech,
'extra_data': extra_data,
}
@callback
def async_set_card(self, title, content, card_type='simple'):
"""Set speech response."""
self.card[card_type] = {
'title': title,
'content': content,
}
@callback
def as_dict(self):
"""Return a dictionary representation of an intent response."""
return {
'speech': self.speech,
'card': self.card,
}

View file

@ -14,9 +14,7 @@ from aiohttp import web
from homeassistant import core as ha, loader
from homeassistant.setup import setup_component, async_setup_component
from homeassistant.config import async_process_component_config
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.restore_state import DATA_RESTORE_CACHE
from homeassistant.helpers import intent, dispatcher, entity, restore_state
from homeassistant.util.unit_system import METRIC_SYSTEM
import homeassistant.util.dt as date_util
import homeassistant.util.yaml as yaml
@ -193,12 +191,31 @@ def async_mock_service(hass, domain, service):
mock_service = threadsafe_callback_factory(async_mock_service)
@ha.callback
def async_mock_intent(hass, intent_typ):
"""Set up a fake intent handler."""
intents = []
class MockIntentHandler(intent.IntentHandler):
intent_type = intent_typ
@asyncio.coroutine
def async_handle(self, intent):
"""Handle the intent."""
intents.append(intent)
return intent.create_response()
intent.async_register(hass, MockIntentHandler())
return intents
@ha.callback
def async_fire_mqtt_message(hass, topic, payload, qos=0):
"""Fire the MQTT message."""
if isinstance(payload, str):
payload = payload.encode('utf-8')
async_dispatcher_send(
dispatcher.async_dispatcher_send(
hass, mqtt.SIGNAL_MQTT_MESSAGE_RECEIVED, topic,
payload, qos)
@ -352,7 +369,7 @@ class MockPlatform(object):
self._setup_platform(hass, config, add_devices, discovery_info)
class MockToggleDevice(ToggleEntity):
class MockToggleDevice(entity.ToggleEntity):
"""Provide a mock toggle device."""
def __init__(self, name, state):
@ -506,10 +523,11 @@ def init_recorder_component(hass, add_config=None):
def mock_restore_cache(hass, states):
"""Mock the DATA_RESTORE_CACHE."""
hass.data[DATA_RESTORE_CACHE] = {
key = restore_state.DATA_RESTORE_CACHE
hass.data[key] = {
state.entity_id: state for state in states}
_LOGGER.debug('Restore cache: %s', hass.data[DATA_RESTORE_CACHE])
assert len(hass.data[DATA_RESTORE_CACHE]) == len(states), \
_LOGGER.debug('Restore cache: %s', hass.data[key])
assert len(hass.data[key]) == len(states), \
"Duplicate entity_id? {}".format(states)
hass.state = ha.CoreState.starting
mock_component(hass, recorder.DOMAIN)

View file

@ -47,10 +47,14 @@ def alexa_client(loop, hass, test_client):
"uid": "uuid"
}
},
"intents": {
}
}))
assert loop.run_until_complete(async_setup_component(
hass, 'intent_script', {
'intent_script': {
"WhereAreWeIntent": {
"speech": {
"type": "plaintext",
"type": "plain",
"text":
"""
{%- if is_state("device_tracker.paulus", "home")
@ -69,19 +73,19 @@ def alexa_client(loop, hass, test_client):
},
"GetZodiacHoroscopeIntent": {
"speech": {
"type": "plaintext",
"type": "plain",
"text": "You told us your sign is {{ ZodiacSign }}.",
}
},
"AMAZON.PlaybackAction<object@MusicCreativeWork>": {
"speech": {
"type": "plaintext",
"type": "plain",
"text": "Playing {{ object_byArtist_name }}.",
}
},
"CallServiceIntent": {
"speech": {
"type": "plaintext",
"type": "plain",
"text": "Service called",
},
"action": {
@ -93,8 +97,7 @@ def alexa_client(loop, hass, test_client):
}
}
}
}
}))
}))
return loop.run_until_complete(test_client(hass.http.app))

View file

@ -57,14 +57,15 @@ def setUpModule():
hass.services.register("test", "apiai", mock_service)
setup.setup_component(hass, apiai.DOMAIN, {
# Key is here to verify we allow other keys in config too
"homeassistant": {},
"apiai": {
"intents": {
"WhereAreWeIntent": {
"speech":
"""
assert setup.setup_component(hass, apiai.DOMAIN, {
"apiai": {},
})
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") -%}
@ -77,19 +78,25 @@ def setUpModule():
}}
{% endif %}
""",
}
},
"GetZodiacHoroscopeIntent": {
"speech": {
"type": "plain",
"text": "You told us your sign is {{ ZodiacSign }}.",
}
},
"CallServiceIntent": {
"speech": {
"type": "plain",
"text": "Service called",
},
"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",
}
"action": {
"service": "test.apiai",
"data_template": {
"hello": "{{ ZodiacSign }}"
},
"entity_id": "switch.test",
}
}
}
@ -509,5 +516,4 @@ class TestApiai(unittest.TestCase):
self.assertEqual(200, req.status_code)
text = req.json().get("speech")
self.assertEqual(
"Intent 'unknown' is not yet configured within Home Assistant.",
text)
"This intent is not yet configured within Home Assistant.", text)

View file

@ -1,16 +1,18 @@
"""The tests for the Conversation component."""
# pylint: disable=protected-access
import asyncio
import unittest
from unittest.mock import patch
from homeassistant.core import callback
from homeassistant.setup import setup_component
from homeassistant.setup import setup_component, async_setup_component
import homeassistant.components as core_components
from homeassistant.components import conversation
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.util.async import run_coroutine_threadsafe
from homeassistant.helpers import intent
from tests.common import get_test_home_assistant, assert_setup_component
from tests.common import get_test_home_assistant, async_mock_intent
class TestConversation(unittest.TestCase):
@ -25,10 +27,9 @@ class TestConversation(unittest.TestCase):
self.assertTrue(run_coroutine_threadsafe(
core_components.async_setup(self.hass, {}), self.hass.loop
).result())
with assert_setup_component(0):
self.assertTrue(setup_component(self.hass, conversation.DOMAIN, {
conversation.DOMAIN: {}
}))
self.assertTrue(setup_component(self.hass, conversation.DOMAIN, {
conversation.DOMAIN: {}
}))
# pylint: disable=invalid-name
def tearDown(self):
@ -119,44 +120,131 @@ class TestConversation(unittest.TestCase):
self.assertFalse(mock_call.called)
class TestConfiguration(unittest.TestCase):
"""Test the conversation configuration component."""
@asyncio.coroutine
def test_calling_intent(hass):
"""Test calling an intent from a conversation."""
intents = async_mock_intent(hass, 'OrderBeer')
# pylint: disable=invalid-name
def setUp(self):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.assertTrue(setup_component(self.hass, conversation.DOMAIN, {
conversation.DOMAIN: {
'test_2': {
'sentence': 'switch boolean',
'action': {
'service': 'input_boolean.toggle'
}
}
result = yield from async_setup_component(hass, 'conversation', {
'conversation': {
'intents': {
'OrderBeer': [
'I would like the {type} beer'
]
}
}))
}
})
assert result
# pylint: disable=invalid-name
def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()
yield from hass.services.async_call(
'conversation', 'process', {
conversation.ATTR_TEXT: 'I would like the Grolsch beer'
})
yield from hass.async_block_till_done()
def test_custom(self):
"""Setup and perform good turn on requests."""
calls = []
assert len(intents) == 1
intent = intents[0]
assert intent.platform == 'conversation'
assert intent.intent_type == 'OrderBeer'
assert intent.slots == {'type': {'value': 'Grolsch'}}
assert intent.text_input == 'I would like the Grolsch beer'
@callback
def record_call(service):
"""Recorder for a call."""
calls.append(service)
self.hass.services.register('input_boolean', 'toggle', record_call)
@asyncio.coroutine
def test_register_before_setup(hass):
"""Test calling an intent from a conversation."""
intents = async_mock_intent(hass, 'OrderBeer')
event_data = {conversation.ATTR_TEXT: 'switch boolean'}
self.assertTrue(self.hass.services.call(
conversation.DOMAIN, 'process', event_data, True))
hass.components.conversation.async_register('OrderBeer', [
'A {type} beer, please'
])
call = calls[-1]
self.assertEqual('input_boolean', call.domain)
self.assertEqual('toggle', call.service)
result = yield from async_setup_component(hass, 'conversation', {
'conversation': {
'intents': {
'OrderBeer': [
'I would like the {type} beer'
]
}
}
})
assert result
yield from hass.services.async_call(
'conversation', 'process', {
conversation.ATTR_TEXT: 'A Grolsch beer, please'
})
yield from hass.async_block_till_done()
assert len(intents) == 1
intent = intents[0]
assert intent.platform == 'conversation'
assert intent.intent_type == 'OrderBeer'
assert intent.slots == {'type': {'value': 'Grolsch'}}
assert intent.text_input == 'A Grolsch beer, please'
yield from hass.services.async_call(
'conversation', 'process', {
conversation.ATTR_TEXT: 'I would like the Grolsch beer'
})
yield from hass.async_block_till_done()
assert len(intents) == 2
intent = intents[1]
assert intent.platform == 'conversation'
assert intent.intent_type == 'OrderBeer'
assert intent.slots == {'type': {'value': 'Grolsch'}}
assert intent.text_input == 'I would like the Grolsch beer'
@asyncio.coroutine
def test_http_processing_intent(hass, test_client):
"""Test processing intent via HTTP API."""
class TestIntentHandler(intent.IntentHandler):
intent_type = 'OrderBeer'
@asyncio.coroutine
def async_handle(self, intent):
"""Handle the intent."""
response = intent.create_response()
response.async_set_speech(
"I've ordered a {}!".format(intent.slots['type']['value']))
response.async_set_card(
"Beer ordered",
"You chose a {}.".format(intent.slots['type']['value']))
return response
intent.async_register(hass, TestIntentHandler())
result = yield from async_setup_component(hass, 'conversation', {
'conversation': {
'intents': {
'OrderBeer': [
'I would like the {type} beer'
]
}
}
})
assert result
client = yield from test_client(hass.http.app)
resp = yield from client.post('/api/conversation/process', json={
'text': 'I would like the Grolsch beer'
})
assert resp.status == 200
data = yield from resp.json()
assert data == {
'card': {
'simple': {
'content': 'You chose a Grolsch.',
'title': 'Beer ordered'
}},
'speech': {
'plain': {
'extra_data': None,
'speech': "I've ordered a Grolsch!"
}
}
}

View file

@ -1,52 +1,62 @@
"""The tests for the Demo component."""
import asyncio
import json
import os
import unittest
from homeassistant.setup import setup_component
import pytest
from homeassistant.setup import async_setup_component
from homeassistant.components import demo, device_tracker
from homeassistant.remote import JSONEncoder
from tests.common import mock_http_component, get_test_home_assistant
@pytest.fixture
def minimize_demo_platforms(hass):
"""Cleanup demo component for tests."""
orig = demo.COMPONENTS_WITH_DEMO_PLATFORM
demo.COMPONENTS_WITH_DEMO_PLATFORM = [
'switch', 'light', 'media_player']
yield
demo.COMPONENTS_WITH_DEMO_PLATFORM = orig
class TestDemo(unittest.TestCase):
"""Test the Demo component."""
@pytest.fixture(autouse=True)
def demo_cleanup(hass):
"""Clean up device tracker demo file."""
yield
try:
os.remove(hass.config.path(device_tracker.YAML_DEVICES))
except FileNotFoundError:
pass
def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
mock_http_component(self.hass)
def tearDown(self): # pylint: disable=invalid-name
"""Stop everything that was started."""
self.hass.stop()
@asyncio.coroutine
def test_if_demo_state_shows_by_default(hass, minimize_demo_platforms):
"""Test if demo state shows if we give no configuration."""
yield from async_setup_component(hass, demo.DOMAIN, {demo.DOMAIN: {}})
try:
os.remove(self.hass.config.path(device_tracker.YAML_DEVICES))
except FileNotFoundError:
pass
assert hass.states.get('a.Demo_Mode') is not None
def test_if_demo_state_shows_by_default(self):
"""Test if demo state shows if we give no configuration."""
setup_component(self.hass, demo.DOMAIN, {demo.DOMAIN: {}})
self.assertIsNotNone(self.hass.states.get('a.Demo_Mode'))
@asyncio.coroutine
def test_hiding_demo_state(hass, minimize_demo_platforms):
"""Test if you can hide the demo card."""
yield from async_setup_component(hass, demo.DOMAIN, {
demo.DOMAIN: {'hide_demo_state': 1}})
def test_hiding_demo_state(self):
"""Test if you can hide the demo card."""
setup_component(self.hass, demo.DOMAIN, {
demo.DOMAIN: {'hide_demo_state': 1}})
assert hass.states.get('a.Demo_Mode') is None
self.assertIsNone(self.hass.states.get('a.Demo_Mode'))
def test_all_entities_can_be_loaded_over_json(self):
"""Test if you can hide the demo card."""
setup_component(self.hass, demo.DOMAIN, {
demo.DOMAIN: {'hide_demo_state': 1}})
@asyncio.coroutine
def test_all_entities_can_be_loaded_over_json(hass):
"""Test if you can hide the demo card."""
yield from async_setup_component(hass, demo.DOMAIN, {
demo.DOMAIN: {'hide_demo_state': 1}})
try:
json.dumps(self.hass.states.all(), cls=JSONEncoder)
except Exception:
self.fail('Unable to convert all demo entities to JSON. '
'Wrong data in state machine!')
try:
json.dumps(hass.states.async_all(), cls=JSONEncoder)
except Exception:
pytest.fail('Unable to convert all demo entities to JSON. '
'Wrong data in state machine!')

View file

@ -0,0 +1,45 @@
"""Test intent_script component."""
import asyncio
from homeassistant.bootstrap import async_setup_component
from homeassistant.helpers import intent
from tests.common import async_mock_service
@asyncio.coroutine
def test_intent_script(hass):
"""Test intent scripts work."""
calls = async_mock_service(hass, 'test', 'service')
yield from async_setup_component(hass, 'intent_script', {
'intent_script': {
'HelloWorld': {
'action': {
'service': 'test.service',
'data_template': {
'hello': '{{ name }}'
}
},
'card': {
'title': 'Hello {{ name }}',
'content': 'Content for {{ name }}',
},
'speech': {
'text': 'Good morning {{ name }}'
}
}
}
})
response = yield from intent.async_handle(
hass, 'test', 'HelloWorld', {'name': {'value': 'Paulus'}}
)
assert len(calls) == 1
assert calls[0].data['hello'] == 'Paulus'
assert response.speech['plain']['speech'] == 'Good morning Paulus'
assert response.card['simple']['title'] == 'Hello Paulus'
assert response.card['simple']['content'] == 'Content for Paulus'

View file

@ -0,0 +1,61 @@
"""Test shopping list component."""
import asyncio
from homeassistant.bootstrap import async_setup_component
from homeassistant.helpers import intent
@asyncio.coroutine
def test_add_item(hass):
"""Test adding an item intent."""
yield from async_setup_component(hass, 'shopping_list', {})
response = yield from intent.async_handle(
hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'beer'}}
)
assert response.speech['plain']['speech'] == \
"I've added beer to your shopping list"
@asyncio.coroutine
def test_recent_items_intent(hass):
"""Test recent items."""
yield from async_setup_component(hass, 'shopping_list', {})
yield from intent.async_handle(
hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'beer'}}
)
yield from intent.async_handle(
hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'wine'}}
)
yield from intent.async_handle(
hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'soda'}}
)
response = yield from intent.async_handle(
hass, 'test', 'HassShoppingListLastItems'
)
assert response.speech['plain']['speech'] == \
"These are the top 5 items in your shopping list: soda, wine, beer"
@asyncio.coroutine
def test_api(hass, test_client):
"""Test the API."""
yield from async_setup_component(hass, 'shopping_list', {})
yield from intent.async_handle(
hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'beer'}}
)
yield from intent.async_handle(
hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'wine'}}
)
client = yield from test_client(hass.http.app)
resp = yield from client.get('/api/shopping_list')
assert resp.status == 200
data = yield from resp.json()
assert data == ['beer', 'wine']

View file

@ -2,7 +2,7 @@
import asyncio
from homeassistant.bootstrap import async_setup_component
from tests.common import async_fire_mqtt_message, async_mock_service
from tests.common import async_fire_mqtt_message, async_mock_intent
EXAMPLE_MSG = """
{
@ -16,7 +16,7 @@ EXAMPLE_MSG = """
"slotName": "light_color",
"value": {
"kind": "Custom",
"value": "blue"
"value": "green"
}
}
]
@ -27,27 +27,19 @@ EXAMPLE_MSG = """
@asyncio.coroutine
def test_snips_call_action(hass, mqtt_mock):
"""Test calling action via Snips."""
calls = async_mock_service(hass, 'test', 'service')
result = yield from async_setup_component(hass, "snips", {
"snips": {
"intents": {
"Lights": {
"action": {
"service": "test.service",
"data_template": {
"color": "{{ light_color }}"
}
}
}
}
}
"snips": {},
})
assert result
intents = async_mock_intent(hass, 'Lights')
async_fire_mqtt_message(hass, 'hermes/nlu/intentParsed',
EXAMPLE_MSG)
yield from hass.async_block_till_done()
assert len(calls) == 1
call = calls[0]
assert call.data.get('color') == 'blue'
assert len(intents) == 1
intent = intents[0]
assert intent.platform == 'snips'
assert intent.intent_type == 'Lights'
assert intent.slots == {'light_color': {'value': 'green'}}
assert intent.text_input == 'turn the lights green'