-
-
Notifications
You must be signed in to change notification settings - Fork 31.8k
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
Move HassIntent handler code into helpers/intent #12181
Conversation
homeassistant/helpers/intent.py
Outdated
@@ -80,6 +84,17 @@ class IntentHandleError(IntentError): | |||
|
|||
pass | |||
|
|||
@callback |
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.
expected 2 blank lines, found 1
homeassistant/helpers/intent.py
Outdated
hass = intent_obj.hass | ||
slots = self.async_validate_slots(intent_obj.slots) | ||
name = slots['name']['value'] | ||
entity = _match_entity(hass, name) |
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 retrieving a state not an entity.
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.
It;s a roundabout way of retrieving all entites, by getting all states and using the entities that belong to them. Need to add filtering by domain.
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.
The state machine doesn't store entities, it stores states. You should change the variable name to reflect what it is.
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.
The function returns the entity_id though and not the state.
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.
return hass.states.get(entity_id) if entity_id else None
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.
No, it returns a state:
https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/core.py#L662-L668
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.
Ah, I see what you mean. I didn't write that bit. It would be more confusing to return a state, so rewrote the return
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.
Let's rename it to entity_id, as that is what is returned from _match_entity
.
homeassistant/const.py
Outdated
@@ -165,6 +165,11 @@ | |||
EVENT_LOGBOOK_ENTRY = 'logbook_entry' | |||
EVENT_THEMES_UPDATED = 'themes_updated' | |||
|
|||
# #### INTENTS #### |
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.
I think it's fine to keep these defined inside helpers.intent .
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.
That works, components can just import them from intents, just felt a little cleaner to put the in consts but all good.
homeassistant/helpers/intent.py
Outdated
|
||
REQUIREMENTS = ['fuzzywuzzy==0.16.0', 'python-Levenshtein==0.12.0'] |
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.
Helpers can't have requirements.
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.
Yeah, i saw that, but the problem in this particular case is that component/init.py isn't considered a module so doesn't allow requirements. I could put it anywhere under components but that raises a dependency issue.
homeassistant/helpers/intent.py
Outdated
@@ -2,19 +2,26 @@ | |||
import asyncio | |||
import logging | |||
|
|||
import fuzzywuzzy |
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.
'fuzzywuzzy' imported but unused
@balloob @MartinHjelmare So I think I am done with this piece of it, I will do separate PRs to add intents for other components. |
@@ -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') |
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.
We should add this back where we import fuzzywuzzy. I remember it spamming a lot.
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.
I see you've removed fuzzywuzzy, ok too 👍
homeassistant/helpers/intent.py
Outdated
class ServiceIntentHandler(IntentHandler): | ||
"""Intent handler registration.""" | ||
|
||
domain = None |
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.
Would it make sense to take these in the constructor? That way you don't have to specify 3 classes in components/__init__.py
.
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.
Do you mean by extending/creating a different async_register?
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.
class ServiceIntentHandler(IntentHandler):
def __init__(self, domain, service, response):
self.domain = domain
self.service = service
self.response = response
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.
Well, when you put it that way :-) Thanks again.
homeassistant/helpers/intent.py
Outdated
return [x for _, _, x in sorted(matches)] | ||
|
||
|
||
class ServiceIntentHandler(IntentHandler): |
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.
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 entity_id
in the service
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.
Updated. I think having the slot schema in the class also makes it clearer.
script/gen_requirements_all.py
Outdated
@@ -66,6 +66,7 @@ | |||
'pushbullet.py', | |||
'py-canary', | |||
'pydispatcher', | |||
'python-Levenshtein', |
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.
No longer needed
I think we can get away without using fuzzy, I have a function that does a decent job of matching the entities and removing fuzzy makes things cleaner if a little less tolerant. No intent depends on conversation, or anything odd in __init>>. Take a look and see if this looks better. I still need to add tests for intents to test_init and address your other bits. |
tests/components/test_init.py
Outdated
assert call.service == 'toggle' | ||
assert call.data == {'entity_id': ['light.test_light']} | ||
|
||
@asyncio.coroutine |
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.
expected 2 blank lines, found 1
tests/components/test_init.py
Outdated
assert call.service == 'turn_off' | ||
assert call.data == {'entity_id': ['light.test_light']} | ||
|
||
@asyncio.coroutine |
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.
expected 2 blank lines, found 1
tests/components/test_init.py
Outdated
assert call.service == 'turn_on' | ||
assert call.data == {'entity_id': ['light.test_light']} | ||
|
||
@asyncio.coroutine |
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.
expected 2 blank lines, found 1
tests/components/test_init.py
Outdated
@@ -195,3 +198,91 @@ def test_check_config(self, mock_check, mock_stop): | |||
self.hass.block_till_done() | |||
assert mock_check.called | |||
assert not mock_stop.called | |||
|
|||
@asyncio.coroutine |
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.
expected 2 blank lines, found 1
tests/components/test_init.py
Outdated
@@ -11,6 +12,8 @@ | |||
from homeassistant.const import ( | |||
STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) | |||
import homeassistant.components as comps | |||
import homeassistant.components.group as group |
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.
'homeassistant.components.group' imported but unused
tests/components/test_init.py
Outdated
@@ -2,6 +2,7 @@ | |||
# pylint: disable=protected-access | |||
import asyncio | |||
import unittest | |||
import inspect |
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.
'inspect' imported but unused
tests/components/test_init.py
Outdated
assert call.service == 'toggle' | ||
assert call.data == {'entity_id': ['light.test_light']} | ||
|
||
@asyncio.coroutine |
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.
expected 2 blank lines, found 1
tests/components/test_init.py
Outdated
assert call.service == 'turn_off' | ||
assert call.data == {'entity_id': ['light.test_light']} | ||
|
||
@asyncio.coroutine |
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.
expected 2 blank lines, found 1
tests/components/test_init.py
Outdated
assert call.service == 'turn_on' | ||
assert call.data == {'entity_id': ['light.test_light']} | ||
|
||
@asyncio.coroutine |
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.
expected 2 blank lines, found 1
tests/components/test_init.py
Outdated
@@ -195,3 +198,91 @@ def test_check_config(self, mock_check, mock_stop): | |||
self.hass.block_till_done() | |||
assert mock_check.called | |||
assert not mock_stop.called | |||
|
|||
@asyncio.coroutine |
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.
expected 2 blank lines, found 1
tests/components/test_init.py
Outdated
@@ -11,6 +12,8 @@ | |||
from homeassistant.const import ( | |||
STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) | |||
import homeassistant.components as comps | |||
import homeassistant.components.group as group |
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.
'homeassistant.components.group' imported but unused
tests/components/test_init.py
Outdated
@@ -2,6 +2,7 @@ | |||
# pylint: disable=protected-access | |||
import asyncio | |||
import unittest | |||
import inspect |
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.
'inspect' imported but unused
homeassistant/helpers/intent.py
Outdated
name = slots['name']['value'] | ||
entities = {state.entity_id: state.name for state | ||
in hass.states.async_all()} | ||
entity_name = name.replace(' ', '_').lower() |
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.
why ?
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.
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.
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.
We match against entity name and not id right?
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.
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.
homeassistant/helpers/intent.py
Outdated
entities = {state.entity_id: state.name for state | ||
in hass.states.async_all()} | ||
entity_name = name.replace(' ', '_').lower() | ||
entity_name = entity_name.replace('the_', '') |
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.
Please add a comment why this is? Also, it seems very English focused
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.
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.
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 sounds like an issue that should be fixed by things launching intents?
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.
Yep, makes sense that it would be done by extending utterances instead.
homeassistant/helpers/intent.py
Outdated
entity_name = entity_name.replace('the_', '') | ||
|
||
matches = fuzzymatch(entity_name, entities) | ||
entity_id = next((x for x in matches if self.domain in x), None) |
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.
What's with this check? Isn't domain for now always homeassistant
and we don't have those entities?
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.
Well my plan is to add things like covers and media players, so an Intent like HassOpenCover declares it's domain we then match against cover.garage_door and not switch.garage_door or sensor.garage_door.
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.
I think that it's:
- out of scope for the current implementation (since it only works with
homeassistant
- Filter should be applied when generating the dictionary of potential entities to match instead of filtering it after matching.
- A "limit matching to domain" should be an optional boolean passed into the constructor
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.
Why would we add another passed variable instead of just 'if domain not Homeassistant'? And I don't think it's out of scope since the whole intention of this is to lay some groundwork for adding other intents. Otherwise all of this has just been rearranging things with zero added value beyond cleanup.
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.
Can you 100% guarantee that you can always derive the filter from the service domain? Because if you can't, an extra variable is necessary.
A cleanup so that you can start building on top is perfectly fine to be in a single pr. If you're adding functionality you need to use it and you need to test it. Neither is happening.
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.
The best filter would be to filter by entites that support the given domain.service
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.
Correct. However, one thing is that Home Assistant core should not be aware of the different components. So if we would want something like this, we would have to make it part of service registration, and then the service registry needs to know about which attribute should map to the entity… it's a bit too much. So I think that passing the right filter at the moment we define the intent is great, that way the logic remains inside the components. (light intent will specify light filter, etc)
homeassistant/helpers/intent.py
Outdated
yield from hass.services.async_call( | ||
self.domain, self.service, { | ||
ATTR_ENTITY_ID: entity_id | ||
}, blocking=True) |
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.
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.
homeassistant/helpers/intent.py
Outdated
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.", name) |
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.
Please remove period at the end.
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.
🐬 🎉
Only thing left is to see if all the additions of result = yield from component.async_setup(hass, {})
to the conversation component tests are necessary and removed if not.
Description:
This moves the HassIntent handler code into intent in preparation for adding default intents to other components.
Related issue (if applicable): fixes #12087
Pull request in home-assistant.github.io with documentation (if applicable): home-assistant/home-assistant.github.io#N/A
Example entry for
configuration.yaml
(if applicable):Checklist:
If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
tox
run successfully. Your PR cannot be merged unless tests passREQUIREMENTS
variable (example).requirements_all.txt
by runningscript/gen_requirements_all.py
..coveragerc
.If the code does not interact with devices:
tox
run successfully. Your PR cannot be merged unless tests pass