-
-
Notifications
You must be signed in to change notification settings - Fork 38.2k
Move HassIntent handler code into helpers/intent #12181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 18 commits
eedb3d0
bf015c9
b3d7bf6
8651566
c842be2
04b407a
c479075
13d0d9a
ac5d2e5
2dcfe18
861cdc5
2214096
90280d7
f287689
7992a73
5e987e8
5496efd
cb8ab73
2d9becd
0d955d3
1262ba3
ed1bcf2
6cfce83
95d30c6
42273fc
a43bc92
b2ad2f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,30 +7,24 @@ | |
| import asyncio | ||
| import logging | ||
| import re | ||
| import warnings | ||
|
|
||
| import voluptuous as vol | ||
|
|
||
| from homeassistant import core | ||
| from homeassistant.components import http | ||
| from homeassistant.const import ( | ||
| ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) | ||
| from homeassistant.helpers import config_validation as cv | ||
| from homeassistant.helpers import intent | ||
| from homeassistant.helpers.intent import (INTENT_TURN_ON, INTENT_TURN_OFF, | ||
| INTENT_TOGGLE) | ||
| from homeassistant.loader import bind_hass | ||
|
|
||
| REQUIREMENTS = ['fuzzywuzzy==0.16.0'] | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| ATTR_TEXT = 'text' | ||
|
|
||
| DEPENDENCIES = ['http'] | ||
| DOMAIN = 'conversation' | ||
|
|
||
| INTENT_TURN_OFF = 'HassTurnOff' | ||
| INTENT_TURN_ON = 'HassTurnOn' | ||
|
|
||
| REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)') | ||
| REGEX_TYPE = type(re.compile('')) | ||
|
|
||
|
|
@@ -50,7 +44,7 @@ | |
| @core.callback | ||
| @bind_hass | ||
| def async_register(hass, intent_type, utterances): | ||
| """Register an intent. | ||
| """Register utterances and any custom intents. | ||
|
|
||
| Registrations don't require conversations to be loaded. They will become | ||
| active once the conversation component is loaded. | ||
|
|
@@ -75,8 +69,6 @@ def async_register(hass, intent_type, utterances): | |
| @asyncio.coroutine | ||
| def async_setup(hass, config): | ||
| """Register the process service.""" | ||
| warnings.filterwarnings('ignore', module='fuzzywuzzy') | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should add this back where we import fuzzywuzzy. I remember it spamming a lot.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see you've removed fuzzywuzzy, ok too 👍 |
||
|
|
||
| config = config.get(DOMAIN, {}) | ||
| intents = hass.data.get(DOMAIN) | ||
|
|
||
|
|
@@ -102,12 +94,12 @@ def process(service): | |
|
|
||
| hass.http.register_view(ConversationProcessView) | ||
|
|
||
| hass.helpers.intent.async_register(TurnOnIntent()) | ||
| hass.helpers.intent.async_register(TurnOffIntent()) | ||
| async_register(hass, INTENT_TURN_ON, | ||
| ['Turn {name} on', 'Turn on {name}']) | ||
| async_register(hass, INTENT_TURN_OFF, [ | ||
| 'Turn {name} off', 'Turn off {name}']) | ||
| async_register(hass, INTENT_TURN_OFF, | ||
| ['Turn {name} off', 'Turn off {name}']) | ||
| async_register(hass, INTENT_TOGGLE, | ||
| ['Toggle {name}', '{name} toggle']) | ||
|
|
||
| return True | ||
|
|
||
|
|
@@ -151,79 +143,6 @@ def _process(hass, text): | |
| return response | ||
|
|
||
|
|
||
| @core.callback | ||
| def _match_entity(hass, name): | ||
| """Match a name to an entity.""" | ||
| from fuzzywuzzy import process as fuzzyExtract | ||
| entities = {state.entity_id: state.name for state | ||
| in hass.states.async_all()} | ||
| entity_id = fuzzyExtract.extractOne( | ||
| name, entities, score_cutoff=65)[2] | ||
| return hass.states.get(entity_id) if entity_id else None | ||
|
|
||
|
|
||
| class TurnOnIntent(intent.IntentHandler): | ||
| """Handle turning item on intents.""" | ||
|
|
||
| intent_type = INTENT_TURN_ON | ||
| slot_schema = { | ||
| 'name': cv.string, | ||
| } | ||
|
|
||
| @asyncio.coroutine | ||
| def async_handle(self, intent_obj): | ||
| """Handle turn on intent.""" | ||
| hass = intent_obj.hass | ||
| slots = self.async_validate_slots(intent_obj.slots) | ||
| name = slots['name']['value'] | ||
| entity = _match_entity(hass, name) | ||
|
|
||
| if not entity: | ||
| _LOGGER.error("Could not find entity id for %s", name) | ||
| return None | ||
|
|
||
| yield from hass.services.async_call( | ||
| core.DOMAIN, SERVICE_TURN_ON, { | ||
| ATTR_ENTITY_ID: entity.entity_id, | ||
| }, blocking=True) | ||
|
|
||
| response = intent_obj.create_response() | ||
| response.async_set_speech( | ||
| 'Turned on {}'.format(entity.name)) | ||
| return response | ||
|
|
||
|
|
||
| class TurnOffIntent(intent.IntentHandler): | ||
| """Handle turning item off intents.""" | ||
|
|
||
| intent_type = INTENT_TURN_OFF | ||
| slot_schema = { | ||
| 'name': cv.string, | ||
| } | ||
|
|
||
| @asyncio.coroutine | ||
| def async_handle(self, intent_obj): | ||
| """Handle turn off intent.""" | ||
| hass = intent_obj.hass | ||
| slots = self.async_validate_slots(intent_obj.slots) | ||
| name = slots['name']['value'] | ||
| entity = _match_entity(hass, name) | ||
|
|
||
| if not entity: | ||
| _LOGGER.error("Could not find entity id for %s", name) | ||
| return None | ||
|
|
||
| yield from hass.services.async_call( | ||
| core.DOMAIN, SERVICE_TURN_OFF, { | ||
| ATTR_ENTITY_ID: entity.entity_id, | ||
| }, blocking=True) | ||
|
|
||
| response = intent_obj.create_response() | ||
| response.async_set_speech( | ||
| 'Turned off {}'.format(entity.name)) | ||
| return response | ||
|
|
||
|
|
||
| class ConversationProcessView(http.HomeAssistantView): | ||
| """View to retrieve shopping list content.""" | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,27 @@ | ||
| """Module to coordinate user intentions.""" | ||
| import asyncio | ||
| import logging | ||
| import re | ||
|
|
||
| import voluptuous as vol | ||
|
|
||
| from homeassistant.core import callback | ||
| from homeassistant.exceptions import HomeAssistantError | ||
| from homeassistant.loader import bind_hass | ||
| from homeassistant.const import ATTR_ENTITY_ID | ||
|
|
||
|
|
||
| DATA_KEY = 'intent' | ||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| # #### INTENTS #### | ||
| INTENT_TURN_OFF = 'HassTurnOff' | ||
| INTENT_TURN_ON = 'HassTurnOn' | ||
| INTENT_TOGGLE = 'HassToggle' | ||
|
|
||
| SLOT_SCHEMA = vol.Schema({ | ||
| }, extra=vol.ALLOW_EXTRA) | ||
|
|
||
| DATA_KEY = 'intent' | ||
|
|
||
| SPEECH_TYPE_PLAIN = 'plain' | ||
| SPEECH_TYPE_SSML = 'ssml' | ||
|
|
||
|
|
@@ -87,7 +94,7 @@ class IntentHandler: | |
| intent_type = None | ||
| slot_schema = None | ||
| _slot_schema = None | ||
| platforms = None | ||
| platforms = [] | ||
|
|
||
| @callback | ||
| def async_can_handle(self, intent_obj): | ||
|
|
@@ -117,6 +124,60 @@ def __repr__(self): | |
| return '<{} - {}>'.format(self.__class__.__name__, self.intent_type) | ||
|
|
||
|
|
||
| def fuzzyfinder(name, entities): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. expected 2 blank lines, found 1 |
||
| """Semi fuzzy matching function.""" | ||
| matches = [] | ||
| pattern = '.*?'.join(name) | ||
| regex = re.compile(pattern) | ||
| for entity in entities: | ||
| match = regex.search(entity) | ||
| if match: | ||
| matches.append((len(match.group()), match.start(), entity)) | ||
| return [x for _, _, x in sorted(matches)] | ||
|
|
||
|
|
||
| class ServiceIntentHandler(IntentHandler): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you specify in both the name and doc of this class that it is only working for services that call entities and that it needs a name slot that will be mapped to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated. I think having the slot schema in the class also makes it clearer. |
||
| """Intent handler registration.""" | ||
|
|
||
| domain = None | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense to take these in the constructor? That way you don't have to specify 3 classes in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean by extending/creating a different async_register?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. class ServiceIntentHandler(IntentHandler):
def __init__(self, domain, service, response):
self.domain = domain
self.service = service
self.response = response
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, when you put it that way :-) Thanks again. |
||
| service = None | ||
| response = '' | ||
|
|
||
| @asyncio.coroutine | ||
| def async_handle(self, intent_obj): | ||
| """Handle the hass intent.""" | ||
| hass = intent_obj.hass | ||
| slots = self.async_validate_slots(intent_obj.slots) | ||
| response = intent_obj.create_response() | ||
|
|
||
| name = slots['name']['value'] | ||
| entities = {state.entity_id: state.name for state | ||
| in hass.states.async_all()} | ||
| entity_name = name.replace(' ', '_').lower() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment didn't stick, but sine we convert spaces to _ in friendly names this is needed to match and entity names will never contain a space. But this is better done in the matching code.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We match against entity name and not id right?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, should be both since that code should create a dict of id: name but apparently does not for whatever reason and is just creating a list of entity_ids. Flipped it to match on name. |
||
| entity_name = entity_name.replace('the_', '') | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a comment why this is? Also, it seems very English focused
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True, and again with the above this may no longer be necessary but then again it might, but this logic should be moved into that function. But a typical utterance is 'Turn on the living room lights' so we get 'the living room lights' for the entity name. Not that we are trying to match everything but it seemed a common enough use. But I can remove it since there is no need to make it too specific and turn on livign room lights is fine.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This sounds like an issue that should be fixed by things launching intents?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, makes sense that it would be done by extending utterances instead. |
||
|
|
||
| matches = fuzzyfinder(entity_name, entities) | ||
| entity_id = matches[0] if matches else None | ||
| _LOGGER.debug("%s matched entity: %s", name, entity_id) | ||
|
|
||
| response = intent_obj.create_response() | ||
| if not entity_id: | ||
| response.async_set_speech( | ||
| "Could not find entity id matching {}".format(name)) | ||
| _LOGGER.error("Could not find entity id matching %s (%s)", name, | ||
| entity_name) | ||
| return response | ||
|
|
||
| yield from hass.services.async_call( | ||
| self.domain, self.service, { | ||
| ATTR_ENTITY_ID: entity_id | ||
| }, blocking=True) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's remove blocking, it has been causing trouble for Alexa/Google Assistant as turning on can sometimes take a long time and we don't actually know if a service is successful. |
||
|
|
||
| response.async_set_speech( | ||
| self.response.format(name)) | ||
| return response | ||
|
|
||
|
|
||
| class Intent: | ||
| """Hold the intent.""" | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,6 +66,7 @@ | |
| 'pushbullet.py', | ||
| 'py-canary', | ||
| 'pydispatcher', | ||
| 'python-Levenshtein', | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No longer needed |
||
| 'PyJWT', | ||
| 'pylitejet', | ||
| 'pymonoprice', | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a double import. Already imported on the line above this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Complains without it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You will have to reference to it as
intent.INTENT_TURN_ONThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, of course :-). Thanks.