core/homeassistant/components/conversation/__init__.py

193 lines
5.5 KiB
Python
Raw Normal View History

"""Support for functionality to have conversations with Home Assistant."""
2015-03-10 07:08:50 +00:00
import logging
import re
import voluptuous as vol
2015-08-17 03:44:46 +00:00
from homeassistant import core
2017-12-29 09:06:39 +00:00
from homeassistant.components import http
2019-07-31 19:25:30 +00:00
from homeassistant.components.cover import INTENT_CLOSE_COVER, INTENT_OPEN_COVER
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.const import EVENT_COMPONENT_LOADED
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv, intent
from homeassistant.loader import bind_hass
from homeassistant.setup import ATTR_COMPONENT
from .util import create_matcher
2015-03-10 07:08:50 +00:00
2017-12-29 09:06:39 +00:00
_LOGGER = logging.getLogger(__name__)
2019-07-31 19:25:30 +00:00
ATTR_TEXT = "text"
2017-12-29 09:06:39 +00:00
2019-07-31 19:25:30 +00:00
DOMAIN = "conversation"
2019-07-31 19:25:30 +00:00
REGEX_TURN_COMMAND = re.compile(r"turn (?P<name>(?: |\w)+) (?P<command>\w+)")
REGEX_TYPE = type(re.compile(""))
2015-03-10 07:08:50 +00:00
UTTERANCES = {
2019-07-31 19:25:30 +00:00
"cover": {
INTENT_OPEN_COVER: ["Open [the] [a] [an] {name}[s]"],
INTENT_CLOSE_COVER: ["Close [the] [a] [an] {name}[s]"],
}
}
2019-07-31 19:25:30 +00:00
SERVICE_PROCESS = "process"
2015-03-10 07:08:50 +00:00
2019-07-31 19:25:30 +00:00
SERVICE_PROCESS_SCHEMA = vol.Schema({vol.Required(ATTR_TEXT): cv.string})
2019-07-31 19:25:30 +00:00
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Optional("intents"): vol.Schema(
{cv.string: vol.All(cv.ensure_list, [cv.string])}
)
}
)
},
extra=vol.ALLOW_EXTRA,
)
@core.callback
@bind_hass
def async_register(hass, intent_type, utterances):
"""Register utterances and any custom intents.
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] = []
for utterance in utterances:
if isinstance(utterance, REGEX_TYPE):
conf.append(utterance)
else:
conf.append(create_matcher(utterance))
async def async_setup(hass, config):
2016-03-08 16:55:57 +00:00
"""Register the process service."""
config = config.get(DOMAIN, {})
intents = hass.data.get(DOMAIN)
if intents is None:
intents = hass.data[DOMAIN] = {}
2015-03-10 07:08:50 +00:00
2019-07-31 19:25:30 +00:00
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)
async def process(service):
2016-03-08 16:55:57 +00:00
"""Parse text into commands."""
text = service.data[ATTR_TEXT]
2019-07-31 19:25:30 +00:00
_LOGGER.debug("Processing: <%s>", text)
try:
await _process(hass, text)
except intent.IntentHandleError as err:
2019-07-31 19:25:30 +00:00
_LOGGER.error("Error processing %s: %s", text, err)
hass.services.async_register(
2019-07-31 19:25:30 +00:00
DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA
)
hass.http.register_view(ConversationProcessView)
# We strip trailing 's' from name because our state matcher will fail
# if a letter is not there. By removing 's' we can match singular and
# plural names.
2019-07-31 19:25:30 +00:00
async_register(
hass,
intent.INTENT_TURN_ON,
["Turn [the] [a] {name}[s] on", "Turn on [the] [a] [an] {name}[s]"],
)
async_register(
hass,
intent.INTENT_TURN_OFF,
["Turn [the] [a] [an] {name}[s] off", "Turn off [the] [a] [an] {name}[s]"],
)
async_register(
hass,
intent.INTENT_TOGGLE,
["Toggle [the] [a] [an] {name}[s]", "[the] [a] [an] {name}[s] toggle"],
)
@callback
def register_utterances(component):
"""Register utterances for a component."""
if component not in UTTERANCES:
return
for intent_type, sentences in UTTERANCES[component].items():
async_register(hass, intent_type, sentences)
@callback
def component_loaded(event):
"""Handle a new component loaded."""
register_utterances(event.data[ATTR_COMPONENT])
hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded)
# Check already loaded components.
for component in hass.config.components:
register_utterances(component)
return True
2015-03-10 07:08:50 +00:00
async 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 = await hass.helpers.intent.async_handle(
2019-07-31 19:25:30 +00:00
DOMAIN,
intent_type,
{key: {"value": value} for key, value in match.groupdict().items()},
text,
)
return response
class ConversationProcessView(http.HomeAssistantView):
"""View to retrieve shopping list content."""
2019-07-31 19:25:30 +00:00
url = "/api/conversation/process"
name = "api:conversation:process"
2019-07-31 19:25:30 +00:00
@RequestDataValidator(vol.Schema({vol.Required("text"): str}))
async def post(self, request, data):
"""Send a request for processing."""
2019-07-31 19:25:30 +00:00
hass = request.app["hass"]
try:
2019-07-31 19:25:30 +00:00
intent_result = await _process(hass, data["text"])
except intent.IntentHandleError as err:
intent_result = intent.IntentResponse()
intent_result.async_set_speech(str(err))
if intent_result is None:
intent_result = intent.IntentResponse()
intent_result.async_set_speech("Sorry, I didn't understand that")
return self.json(intent_result)