rewrite hangouts to use intents instead of commands (#16220)
* rewrite hangouts to use intents instead of commands * small fixes * remove configured_hangouts check and CONFIG_SCHEMA * Lint * add import from .config_flow
This commit is contained in:
parent
2744702f9b
commit
a953601abd
6 changed files with 196 additions and 142 deletions
|
@ -11,6 +11,7 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import core
|
from homeassistant import core
|
||||||
from homeassistant.components import http
|
from homeassistant.components import http
|
||||||
|
from homeassistant.components.conversation.util import create_matcher
|
||||||
from homeassistant.components.http.data_validator import (
|
from homeassistant.components.http.data_validator import (
|
||||||
RequestDataValidator)
|
RequestDataValidator)
|
||||||
from homeassistant.components.cover import (INTENT_OPEN_COVER,
|
from homeassistant.components.cover import (INTENT_OPEN_COVER,
|
||||||
|
@ -74,7 +75,7 @@ def async_register(hass, intent_type, utterances):
|
||||||
if isinstance(utterance, REGEX_TYPE):
|
if isinstance(utterance, REGEX_TYPE):
|
||||||
conf.append(utterance)
|
conf.append(utterance)
|
||||||
else:
|
else:
|
||||||
conf.append(_create_matcher(utterance))
|
conf.append(create_matcher(utterance))
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
|
@ -91,7 +92,7 @@ async def async_setup(hass, config):
|
||||||
if conf is None:
|
if conf is None:
|
||||||
conf = intents[intent_type] = []
|
conf = intents[intent_type] = []
|
||||||
|
|
||||||
conf.extend(_create_matcher(utterance) for utterance in utterances)
|
conf.extend(create_matcher(utterance) for utterance in utterances)
|
||||||
|
|
||||||
async def process(service):
|
async def process(service):
|
||||||
"""Parse text into commands."""
|
"""Parse text into commands."""
|
||||||
|
@ -146,39 +147,6 @@ async def async_setup(hass, config):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _create_matcher(utterance):
|
|
||||||
"""Create a regex that matches the utterance."""
|
|
||||||
# Split utterance into parts that are type: NORMAL, GROUP or OPTIONAL
|
|
||||||
# Pattern matches (GROUP|OPTIONAL): Change light to [the color] {name}
|
|
||||||
parts = re.split(r'({\w+}|\[[\w\s]+\] *)', utterance)
|
|
||||||
# Pattern to extract name from GROUP part. Matches {name}
|
|
||||||
group_matcher = re.compile(r'{(\w+)}')
|
|
||||||
# Pattern to extract text from OPTIONAL part. Matches [the color]
|
|
||||||
optional_matcher = re.compile(r'\[([\w ]+)\] *')
|
|
||||||
|
|
||||||
pattern = ['^']
|
|
||||||
for part in parts:
|
|
||||||
group_match = group_matcher.match(part)
|
|
||||||
optional_match = optional_matcher.match(part)
|
|
||||||
|
|
||||||
# Normal part
|
|
||||||
if group_match is None and optional_match is None:
|
|
||||||
pattern.append(part)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Group part
|
|
||||||
if group_match is not None:
|
|
||||||
pattern.append(
|
|
||||||
r'(?P<{}>[\w ]+?)\s*'.format(group_match.groups()[0]))
|
|
||||||
|
|
||||||
# Optional part
|
|
||||||
elif optional_match is not None:
|
|
||||||
pattern.append(r'(?:{} *)?'.format(optional_match.groups()[0]))
|
|
||||||
|
|
||||||
pattern.append('$')
|
|
||||||
return re.compile(''.join(pattern), re.I)
|
|
||||||
|
|
||||||
|
|
||||||
async def _process(hass, text):
|
async def _process(hass, text):
|
||||||
"""Process a line of text."""
|
"""Process a line of text."""
|
||||||
intents = hass.data.get(DOMAIN, {})
|
intents = hass.data.get(DOMAIN, {})
|
||||||
|
|
35
homeassistant/components/conversation/util.py
Normal file
35
homeassistant/components/conversation/util.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
"""Util for Conversation."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def create_matcher(utterance):
|
||||||
|
"""Create a regex that matches the utterance."""
|
||||||
|
# Split utterance into parts that are type: NORMAL, GROUP or OPTIONAL
|
||||||
|
# Pattern matches (GROUP|OPTIONAL): Change light to [the color] {name}
|
||||||
|
parts = re.split(r'({\w+}|\[[\w\s]+\] *)', utterance)
|
||||||
|
# Pattern to extract name from GROUP part. Matches {name}
|
||||||
|
group_matcher = re.compile(r'{(\w+)}')
|
||||||
|
# Pattern to extract text from OPTIONAL part. Matches [the color]
|
||||||
|
optional_matcher = re.compile(r'\[([\w ]+)\] *')
|
||||||
|
|
||||||
|
pattern = ['^']
|
||||||
|
for part in parts:
|
||||||
|
group_match = group_matcher.match(part)
|
||||||
|
optional_match = optional_matcher.match(part)
|
||||||
|
|
||||||
|
# Normal part
|
||||||
|
if group_match is None and optional_match is None:
|
||||||
|
pattern.append(part)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Group part
|
||||||
|
if group_match is not None:
|
||||||
|
pattern.append(
|
||||||
|
r'(?P<{}>[\w ]+?)\s*'.format(group_match.groups()[0]))
|
||||||
|
|
||||||
|
# Optional part
|
||||||
|
elif optional_match is not None:
|
||||||
|
pattern.append(r'(?:{} *)?'.format(optional_match.groups()[0]))
|
||||||
|
|
||||||
|
pattern.append('$')
|
||||||
|
return re.compile(''.join(pattern), re.I)
|
|
@ -11,25 +11,53 @@ import voluptuous as vol
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.helpers import dispatcher
|
from homeassistant.helpers import dispatcher
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
from .config_flow import configured_hangouts
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_BOT, CONF_COMMANDS, CONF_REFRESH_TOKEN, DOMAIN,
|
CONF_BOT, CONF_INTENTS, CONF_REFRESH_TOKEN, DOMAIN,
|
||||||
EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
|
EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
|
||||||
MESSAGE_SCHEMA, SERVICE_SEND_MESSAGE,
|
MESSAGE_SCHEMA, SERVICE_SEND_MESSAGE,
|
||||||
SERVICE_UPDATE)
|
SERVICE_UPDATE, CONF_SENTENCES, CONF_MATCHERS,
|
||||||
|
CONF_ERROR_SUPPRESSED_CONVERSATIONS, INTENT_SCHEMA, TARGETS_SCHEMA)
|
||||||
|
|
||||||
|
# We need an import from .config_flow, without it .config_flow is never loaded.
|
||||||
|
from .config_flow import HangoutsFlowHandler # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
REQUIREMENTS = ['hangups==0.4.5']
|
REQUIREMENTS = ['hangups==0.4.5']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
|
vol.Optional(CONF_INTENTS, default={}): vol.Schema({
|
||||||
|
cv.string: INTENT_SCHEMA
|
||||||
|
}),
|
||||||
|
vol.Optional(CONF_ERROR_SUPPRESSED_CONVERSATIONS, default=[]):
|
||||||
|
[TARGETS_SCHEMA]
|
||||||
|
})
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up the Hangouts bot component."""
|
"""Set up the Hangouts bot component."""
|
||||||
config = config.get(DOMAIN, {})
|
from homeassistant.components.conversation import create_matcher
|
||||||
hass.data[DOMAIN] = {CONF_COMMANDS: config.get(CONF_COMMANDS, [])}
|
|
||||||
|
config = config.get(DOMAIN)
|
||||||
|
if config is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
hass.data[DOMAIN] = {CONF_INTENTS: config.get(CONF_INTENTS),
|
||||||
|
CONF_ERROR_SUPPRESSED_CONVERSATIONS:
|
||||||
|
config.get(CONF_ERROR_SUPPRESSED_CONVERSATIONS)}
|
||||||
|
|
||||||
|
for data in hass.data[DOMAIN][CONF_INTENTS].values():
|
||||||
|
matchers = []
|
||||||
|
for sentence in data[CONF_SENTENCES]:
|
||||||
|
matchers.append(create_matcher(sentence))
|
||||||
|
|
||||||
|
data[CONF_MATCHERS] = matchers
|
||||||
|
|
||||||
if configured_hangouts(hass) is None:
|
|
||||||
hass.async_add_job(hass.config_entries.flow.async_init(
|
hass.async_add_job(hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={'source': config_entries.SOURCE_IMPORT}
|
DOMAIN, context={'source': config_entries.SOURCE_IMPORT}
|
||||||
))
|
))
|
||||||
|
@ -47,7 +75,8 @@ async def async_setup_entry(hass, config):
|
||||||
bot = HangoutsBot(
|
bot = HangoutsBot(
|
||||||
hass,
|
hass,
|
||||||
config.data.get(CONF_REFRESH_TOKEN),
|
config.data.get(CONF_REFRESH_TOKEN),
|
||||||
hass.data[DOMAIN][CONF_COMMANDS])
|
hass.data[DOMAIN][CONF_INTENTS],
|
||||||
|
hass.data[DOMAIN][CONF_ERROR_SUPPRESSED_CONVERSATIONS])
|
||||||
hass.data[DOMAIN][CONF_BOT] = bot
|
hass.data[DOMAIN][CONF_BOT] = bot
|
||||||
except GoogleAuthError as exception:
|
except GoogleAuthError as exception:
|
||||||
_LOGGER.error("Hangouts failed to log in: %s", str(exception))
|
_LOGGER.error("Hangouts failed to log in: %s", str(exception))
|
||||||
|
@ -62,6 +91,10 @@ async def async_setup_entry(hass, config):
|
||||||
hass,
|
hass,
|
||||||
EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
|
EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
|
||||||
bot.async_update_conversation_commands)
|
bot.async_update_conversation_commands)
|
||||||
|
dispatcher.async_dispatcher_connect(
|
||||||
|
hass,
|
||||||
|
EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
|
||||||
|
bot.async_handle_update_error_suppressed_conversations)
|
||||||
|
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||||
bot.async_handle_hass_stop)
|
bot.async_handle_hass_stop)
|
||||||
|
|
|
@ -4,7 +4,6 @@ import logging
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.notify import ATTR_MESSAGE, ATTR_TARGET
|
from homeassistant.components.notify import ATTR_MESSAGE, ATTR_TARGET
|
||||||
from homeassistant.const import CONF_NAME
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger('homeassistant.components.hangouts')
|
_LOGGER = logging.getLogger('homeassistant.components.hangouts')
|
||||||
|
@ -18,17 +17,18 @@ CONF_BOT = 'bot'
|
||||||
|
|
||||||
CONF_CONVERSATIONS = 'conversations'
|
CONF_CONVERSATIONS = 'conversations'
|
||||||
CONF_DEFAULT_CONVERSATIONS = 'default_conversations'
|
CONF_DEFAULT_CONVERSATIONS = 'default_conversations'
|
||||||
|
CONF_ERROR_SUPPRESSED_CONVERSATIONS = 'error_suppressed_conversations'
|
||||||
|
|
||||||
CONF_COMMANDS = 'commands'
|
CONF_INTENTS = 'intents'
|
||||||
CONF_WORD = 'word'
|
CONF_INTENT_TYPE = 'intent_type'
|
||||||
CONF_EXPRESSION = 'expression'
|
CONF_SENTENCES = 'sentences'
|
||||||
|
CONF_MATCHERS = 'matchers'
|
||||||
EVENT_HANGOUTS_COMMAND = 'hangouts_command'
|
|
||||||
|
|
||||||
EVENT_HANGOUTS_CONNECTED = 'hangouts_connected'
|
EVENT_HANGOUTS_CONNECTED = 'hangouts_connected'
|
||||||
EVENT_HANGOUTS_DISCONNECTED = 'hangouts_disconnected'
|
EVENT_HANGOUTS_DISCONNECTED = 'hangouts_disconnected'
|
||||||
EVENT_HANGOUTS_USERS_CHANGED = 'hangouts_users_changed'
|
EVENT_HANGOUTS_USERS_CHANGED = 'hangouts_users_changed'
|
||||||
EVENT_HANGOUTS_CONVERSATIONS_CHANGED = 'hangouts_conversations_changed'
|
EVENT_HANGOUTS_CONVERSATIONS_CHANGED = 'hangouts_conversations_changed'
|
||||||
|
EVENT_HANGOUTS_MESSAGE_RECEIVED = 'hangouts_message_received'
|
||||||
|
|
||||||
CONF_CONVERSATION_ID = 'id'
|
CONF_CONVERSATION_ID = 'id'
|
||||||
CONF_CONVERSATION_NAME = 'name'
|
CONF_CONVERSATION_NAME = 'name'
|
||||||
|
@ -59,20 +59,10 @@ MESSAGE_SCHEMA = vol.Schema({
|
||||||
vol.Required(ATTR_MESSAGE): [MESSAGE_SEGMENT_SCHEMA]
|
vol.Required(ATTR_MESSAGE): [MESSAGE_SEGMENT_SCHEMA]
|
||||||
})
|
})
|
||||||
|
|
||||||
COMMAND_SCHEMA = vol.All(
|
INTENT_SCHEMA = vol.All(
|
||||||
# Basic Schema
|
# Basic Schema
|
||||||
vol.Schema({
|
vol.Schema({
|
||||||
vol.Exclusive(CONF_WORD, 'trigger'): cv.string,
|
vol.Required(CONF_SENTENCES): vol.All(cv.ensure_list, [cv.string]),
|
||||||
vol.Exclusive(CONF_EXPRESSION, 'trigger'): cv.is_regex,
|
|
||||||
vol.Required(CONF_NAME): cv.string,
|
|
||||||
vol.Optional(CONF_CONVERSATIONS): [TARGETS_SCHEMA]
|
vol.Optional(CONF_CONVERSATIONS): [TARGETS_SCHEMA]
|
||||||
}),
|
}),
|
||||||
# Make sure it's either a word or an expression command
|
|
||||||
cv.has_at_least_one_key(CONF_WORD, CONF_EXPRESSION)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
|
||||||
DOMAIN: vol.Schema({
|
|
||||||
vol.Optional(CONF_COMMANDS, default=[]): [COMMAND_SCHEMA]
|
|
||||||
})
|
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
"""The Hangouts Bot."""
|
"""The Hangouts Bot."""
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
|
|
||||||
from homeassistant.helpers import dispatcher
|
from homeassistant.helpers import dispatcher, intent
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_MESSAGE, ATTR_TARGET, CONF_CONVERSATIONS, CONF_EXPRESSION, CONF_NAME,
|
ATTR_MESSAGE, ATTR_TARGET, CONF_CONVERSATIONS, DOMAIN,
|
||||||
CONF_WORD, DOMAIN, EVENT_HANGOUTS_COMMAND, EVENT_HANGOUTS_CONNECTED,
|
EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED,
|
||||||
EVENT_HANGOUTS_CONVERSATIONS_CHANGED, EVENT_HANGOUTS_DISCONNECTED)
|
EVENT_HANGOUTS_DISCONNECTED, EVENT_HANGOUTS_MESSAGE_RECEIVED,
|
||||||
|
CONF_MATCHERS, CONF_CONVERSATION_ID,
|
||||||
|
CONF_CONVERSATION_NAME)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -15,20 +16,34 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
class HangoutsBot:
|
class HangoutsBot:
|
||||||
"""The Hangouts Bot."""
|
"""The Hangouts Bot."""
|
||||||
|
|
||||||
def __init__(self, hass, refresh_token, commands):
|
def __init__(self, hass, refresh_token, intents, error_suppressed_convs):
|
||||||
"""Set up the client."""
|
"""Set up the client."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._connected = False
|
self._connected = False
|
||||||
|
|
||||||
self._refresh_token = refresh_token
|
self._refresh_token = refresh_token
|
||||||
|
|
||||||
self._commands = commands
|
self._intents = intents
|
||||||
|
self._conversation_intents = None
|
||||||
|
|
||||||
self._word_commands = None
|
|
||||||
self._expression_commands = None
|
|
||||||
self._client = None
|
self._client = None
|
||||||
self._user_list = None
|
self._user_list = None
|
||||||
self._conversation_list = None
|
self._conversation_list = None
|
||||||
|
self._error_suppressed_convs = error_suppressed_convs
|
||||||
|
self._error_suppressed_conv_ids = None
|
||||||
|
|
||||||
|
dispatcher.async_dispatcher_connect(
|
||||||
|
self.hass, EVENT_HANGOUTS_MESSAGE_RECEIVED,
|
||||||
|
self._async_handle_conversation_message)
|
||||||
|
|
||||||
|
def _resolve_conversation_id(self, obj):
|
||||||
|
if CONF_CONVERSATION_ID in obj:
|
||||||
|
return obj[CONF_CONVERSATION_ID]
|
||||||
|
if CONF_CONVERSATION_NAME in obj:
|
||||||
|
conv = self._resolve_conversation_name(obj[CONF_CONVERSATION_NAME])
|
||||||
|
if conv is not None:
|
||||||
|
return conv.id_
|
||||||
|
return None
|
||||||
|
|
||||||
def _resolve_conversation_name(self, name):
|
def _resolve_conversation_name(self, name):
|
||||||
for conv in self._conversation_list.get_all():
|
for conv in self._conversation_list.get_all():
|
||||||
|
@ -38,89 +53,100 @@ class HangoutsBot:
|
||||||
|
|
||||||
def async_update_conversation_commands(self, _):
|
def async_update_conversation_commands(self, _):
|
||||||
"""Refresh the commands for every conversation."""
|
"""Refresh the commands for every conversation."""
|
||||||
self._word_commands = {}
|
self._conversation_intents = {}
|
||||||
self._expression_commands = {}
|
|
||||||
|
|
||||||
for command in self._commands:
|
for intent_type, data in self._intents.items():
|
||||||
if command.get(CONF_CONVERSATIONS):
|
if data.get(CONF_CONVERSATIONS):
|
||||||
conversations = []
|
conversations = []
|
||||||
for conversation in command.get(CONF_CONVERSATIONS):
|
for conversation in data.get(CONF_CONVERSATIONS):
|
||||||
if 'id' in conversation:
|
conv_id = self._resolve_conversation_id(conversation)
|
||||||
conversations.append(conversation['id'])
|
if conv_id is not None:
|
||||||
elif 'name' in conversation:
|
conversations.append(conv_id)
|
||||||
conversations.append(self._resolve_conversation_name(
|
data['_' + CONF_CONVERSATIONS] = conversations
|
||||||
conversation['name']).id_)
|
|
||||||
command['_' + CONF_CONVERSATIONS] = conversations
|
|
||||||
else:
|
else:
|
||||||
command['_' + CONF_CONVERSATIONS] = \
|
data['_' + CONF_CONVERSATIONS] = \
|
||||||
[conv.id_ for conv in self._conversation_list.get_all()]
|
[conv.id_ for conv in self._conversation_list.get_all()]
|
||||||
|
|
||||||
if command.get(CONF_WORD):
|
for conv_id in data['_' + CONF_CONVERSATIONS]:
|
||||||
for conv_id in command['_' + CONF_CONVERSATIONS]:
|
if conv_id not in self._conversation_intents:
|
||||||
if conv_id not in self._word_commands:
|
self._conversation_intents[conv_id] = {}
|
||||||
self._word_commands[conv_id] = {}
|
|
||||||
word = command[CONF_WORD].lower()
|
|
||||||
self._word_commands[conv_id][word] = command
|
|
||||||
elif command.get(CONF_EXPRESSION):
|
|
||||||
command['_' + CONF_EXPRESSION] = re.compile(
|
|
||||||
command.get(CONF_EXPRESSION))
|
|
||||||
|
|
||||||
for conv_id in command['_' + CONF_CONVERSATIONS]:
|
self._conversation_intents[conv_id][intent_type] = data
|
||||||
if conv_id not in self._expression_commands:
|
|
||||||
self._expression_commands[conv_id] = []
|
|
||||||
self._expression_commands[conv_id].append(command)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._conversation_list.on_event.remove_observer(
|
self._conversation_list.on_event.remove_observer(
|
||||||
self._handle_conversation_event)
|
self._async_handle_conversation_event)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
self._conversation_list.on_event.add_observer(
|
self._conversation_list.on_event.add_observer(
|
||||||
self._handle_conversation_event)
|
self._async_handle_conversation_event)
|
||||||
|
|
||||||
def _handle_conversation_event(self, event):
|
def async_handle_update_error_suppressed_conversations(self, _):
|
||||||
|
"""Resolve the list of error suppressed conversations."""
|
||||||
|
self._error_suppressed_conv_ids = []
|
||||||
|
for conversation in self._error_suppressed_convs:
|
||||||
|
conv_id = self._resolve_conversation_id(conversation)
|
||||||
|
if conv_id is not None:
|
||||||
|
self._error_suppressed_conv_ids.append(conv_id)
|
||||||
|
|
||||||
|
async def _async_handle_conversation_event(self, event):
|
||||||
from hangups import ChatMessageEvent
|
from hangups import ChatMessageEvent
|
||||||
if event.__class__ is ChatMessageEvent:
|
if isinstance(event, ChatMessageEvent):
|
||||||
self._handle_conversation_message(
|
dispatcher.async_dispatcher_send(self.hass,
|
||||||
event.conversation_id, event.user_id, event)
|
EVENT_HANGOUTS_MESSAGE_RECEIVED,
|
||||||
|
event.conversation_id,
|
||||||
|
event.user_id, event)
|
||||||
|
|
||||||
def _handle_conversation_message(self, conv_id, user_id, event):
|
async def _async_handle_conversation_message(self,
|
||||||
|
conv_id, user_id, event):
|
||||||
"""Handle a message sent to a conversation."""
|
"""Handle a message sent to a conversation."""
|
||||||
user = self._user_list.get_user(user_id)
|
user = self._user_list.get_user(user_id)
|
||||||
if user.is_self:
|
if user.is_self:
|
||||||
return
|
return
|
||||||
|
message = event.text
|
||||||
|
|
||||||
_LOGGER.debug("Handling message '%s' from %s",
|
_LOGGER.debug("Handling message '%s' from %s",
|
||||||
event.text, user.full_name)
|
message, user.full_name)
|
||||||
|
|
||||||
event_data = None
|
intents = self._conversation_intents.get(conv_id)
|
||||||
|
if intents is not None:
|
||||||
|
is_error = False
|
||||||
|
try:
|
||||||
|
intent_result = await self._async_process(intents, message)
|
||||||
|
except (intent.UnknownIntent, intent.IntentHandleError) as err:
|
||||||
|
is_error = True
|
||||||
|
intent_result = intent.IntentResponse()
|
||||||
|
intent_result.async_set_speech(str(err))
|
||||||
|
|
||||||
|
if intent_result is None:
|
||||||
|
is_error = True
|
||||||
|
intent_result = intent.IntentResponse()
|
||||||
|
intent_result.async_set_speech(
|
||||||
|
"Sorry, I didn't understand that")
|
||||||
|
|
||||||
|
message = intent_result.as_dict().get('speech', {})\
|
||||||
|
.get('plain', {}).get('speech')
|
||||||
|
|
||||||
|
if (message is not None) and not (
|
||||||
|
is_error and conv_id in self._error_suppressed_conv_ids):
|
||||||
|
await self._async_send_message(
|
||||||
|
[{'text': message, 'parse_str': True}],
|
||||||
|
[{CONF_CONVERSATION_ID: conv_id}])
|
||||||
|
|
||||||
|
async def _async_process(self, intents, text):
|
||||||
|
"""Detect a matching intent."""
|
||||||
|
for intent_type, data in intents.items():
|
||||||
|
for matcher in data.get(CONF_MATCHERS, []):
|
||||||
|
match = matcher.match(text)
|
||||||
|
|
||||||
pieces = event.text.split(' ')
|
|
||||||
cmd = pieces[0].lower()
|
|
||||||
command = self._word_commands.get(conv_id, {}).get(cmd)
|
|
||||||
if command:
|
|
||||||
event_data = {
|
|
||||||
'command': command[CONF_NAME],
|
|
||||||
'conversation_id': conv_id,
|
|
||||||
'user_id': user_id,
|
|
||||||
'user_name': user.full_name,
|
|
||||||
'data': pieces[1:]
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
# After single-word commands, check all regex commands in the room
|
|
||||||
for command in self._expression_commands.get(conv_id, []):
|
|
||||||
match = command['_' + CONF_EXPRESSION].match(event.text)
|
|
||||||
if not match:
|
if not match:
|
||||||
continue
|
continue
|
||||||
event_data = {
|
|
||||||
'command': command[CONF_NAME],
|
response = await self.hass.helpers.intent.async_handle(
|
||||||
'conversation_id': conv_id,
|
DOMAIN, intent_type,
|
||||||
'user_id': user_id,
|
{key: {'value': value} for key, value
|
||||||
'user_name': user.full_name,
|
in match.groupdict().items()}, text)
|
||||||
'data': match.groupdict()
|
return response
|
||||||
}
|
|
||||||
if event_data is not None:
|
|
||||||
self.hass.bus.fire(EVENT_HANGOUTS_COMMAND, event_data)
|
|
||||||
|
|
||||||
async def async_connect(self):
|
async def async_connect(self):
|
||||||
"""Login to the Google Hangouts."""
|
"""Login to the Google Hangouts."""
|
||||||
|
@ -163,10 +189,12 @@ class HangoutsBot:
|
||||||
conversations = []
|
conversations = []
|
||||||
for target in targets:
|
for target in targets:
|
||||||
conversation = None
|
conversation = None
|
||||||
if 'id' in target:
|
if CONF_CONVERSATION_ID in target:
|
||||||
conversation = self._conversation_list.get(target['id'])
|
conversation = self._conversation_list.get(
|
||||||
elif 'name' in target:
|
target[CONF_CONVERSATION_ID])
|
||||||
conversation = self._resolve_conversation_name(target['name'])
|
elif CONF_CONVERSATION_NAME in target:
|
||||||
|
conversation = self._resolve_conversation_name(
|
||||||
|
target[CONF_CONVERSATION_NAME])
|
||||||
if conversation is not None:
|
if conversation is not None:
|
||||||
conversations.append(conversation)
|
conversations.append(conversation)
|
||||||
|
|
||||||
|
@ -200,8 +228,8 @@ class HangoutsBot:
|
||||||
users_in_conversation = []
|
users_in_conversation = []
|
||||||
for user in conv.users:
|
for user in conv.users:
|
||||||
users_in_conversation.append(user.full_name)
|
users_in_conversation.append(user.full_name)
|
||||||
conversations[str(i)] = {'id': str(conv.id_),
|
conversations[str(i)] = {CONF_CONVERSATION_ID: str(conv.id_),
|
||||||
'name': conv.name,
|
CONF_CONVERSATION_NAME: conv.name,
|
||||||
'users': users_in_conversation}
|
'users': users_in_conversation}
|
||||||
|
|
||||||
self.hass.states.async_set("{}.conversations".format(DOMAIN),
|
self.hass.states.async_set("{}.conversations".format(DOMAIN),
|
||||||
|
|
|
@ -290,11 +290,11 @@ async def test_http_api_wrong_data(hass, aiohttp_client):
|
||||||
def test_create_matcher():
|
def test_create_matcher():
|
||||||
"""Test the create matcher method."""
|
"""Test the create matcher method."""
|
||||||
# Basic sentence
|
# Basic sentence
|
||||||
pattern = conversation._create_matcher('Hello world')
|
pattern = conversation.create_matcher('Hello world')
|
||||||
assert pattern.match('Hello world') is not None
|
assert pattern.match('Hello world') is not None
|
||||||
|
|
||||||
# Match a part
|
# Match a part
|
||||||
pattern = conversation._create_matcher('Hello {name}')
|
pattern = conversation.create_matcher('Hello {name}')
|
||||||
match = pattern.match('hello world')
|
match = pattern.match('hello world')
|
||||||
assert match is not None
|
assert match is not None
|
||||||
assert match.groupdict()['name'] == 'world'
|
assert match.groupdict()['name'] == 'world'
|
||||||
|
@ -302,7 +302,7 @@ def test_create_matcher():
|
||||||
assert no_match is None
|
assert no_match is None
|
||||||
|
|
||||||
# Optional and matching part
|
# Optional and matching part
|
||||||
pattern = conversation._create_matcher('Turn on [the] {name}')
|
pattern = conversation.create_matcher('Turn on [the] {name}')
|
||||||
match = pattern.match('turn on the kitchen lights')
|
match = pattern.match('turn on the kitchen lights')
|
||||||
assert match is not None
|
assert match is not None
|
||||||
assert match.groupdict()['name'] == 'kitchen lights'
|
assert match.groupdict()['name'] == 'kitchen lights'
|
||||||
|
@ -313,7 +313,7 @@ def test_create_matcher():
|
||||||
assert match is None
|
assert match is None
|
||||||
|
|
||||||
# Two different optional parts, 1 matching part
|
# Two different optional parts, 1 matching part
|
||||||
pattern = conversation._create_matcher('Turn on [the] [a] {name}')
|
pattern = conversation.create_matcher('Turn on [the] [a] {name}')
|
||||||
match = pattern.match('turn on the kitchen lights')
|
match = pattern.match('turn on the kitchen lights')
|
||||||
assert match is not None
|
assert match is not None
|
||||||
assert match.groupdict()['name'] == 'kitchen lights'
|
assert match.groupdict()['name'] == 'kitchen lights'
|
||||||
|
@ -325,13 +325,13 @@ def test_create_matcher():
|
||||||
assert match.groupdict()['name'] == 'kitchen light'
|
assert match.groupdict()['name'] == 'kitchen light'
|
||||||
|
|
||||||
# Strip plural
|
# Strip plural
|
||||||
pattern = conversation._create_matcher('Turn {name}[s] on')
|
pattern = conversation.create_matcher('Turn {name}[s] on')
|
||||||
match = pattern.match('turn kitchen lights on')
|
match = pattern.match('turn kitchen lights on')
|
||||||
assert match is not None
|
assert match is not None
|
||||||
assert match.groupdict()['name'] == 'kitchen light'
|
assert match.groupdict()['name'] == 'kitchen light'
|
||||||
|
|
||||||
# Optional 2 words
|
# Optional 2 words
|
||||||
pattern = conversation._create_matcher('Turn [the great] {name} on')
|
pattern = conversation.create_matcher('Turn [the great] {name} on')
|
||||||
match = pattern.match('turn the great kitchen lights on')
|
match = pattern.match('turn the great kitchen lights on')
|
||||||
assert match is not None
|
assert match is not None
|
||||||
assert match.groupdict()['name'] == 'kitchen lights'
|
assert match.groupdict()['name'] == 'kitchen lights'
|
||||||
|
|
Loading…
Add table
Reference in a new issue