Move HassIntent handler code into helpers/intent#12181
Move HassIntent handler code into helpers/intent#12181balloob merged 27 commits intohome-assistant:devfrom
Conversation
|
|
||
| pass | ||
|
|
||
| @callback |
| 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.
This is retrieving a state not an entity.
There was a problem hiding this comment.
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.
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.
The function returns the entity_id though and not the state.
There was a problem hiding this comment.
return hass.states.get(entity_id) if entity_id else None
There was a problem hiding this comment.
There was a problem hiding this comment.
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.
Let's rename it to entity_id, as that is what is returned from _match_entity.
| EVENT_LOGBOOK_ENTRY = 'logbook_entry' | ||
| EVENT_THEMES_UPDATED = 'themes_updated' | ||
|
|
||
| # #### INTENTS #### |
There was a problem hiding this comment.
I think it's fine to keep these defined inside helpers.intent .
There was a problem hiding this comment.
That works, components can just import them from intents, just felt a little cleaner to put the in consts but all good.
| from homeassistant.loader import bind_hass | ||
| from homeassistant.const import ATTR_ENTITY_ID | ||
|
|
||
| REQUIREMENTS = ['fuzzywuzzy==0.16.0', 'python-Levenshtein==0.12.0'] |
There was a problem hiding this comment.
Helpers can't have requirements.
There was a problem hiding this comment.
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.
| import asyncio | ||
| import logging | ||
|
|
||
| import fuzzywuzzy |
|
@balloob @MartinHjelmare So I think I am done with this piece of it, I will do separate PRs to add intents for other components. |
| @asyncio.coroutine | ||
| def async_setup(hass, config): | ||
| """Register the process service.""" | ||
| warnings.filterwarnings('ignore', module='fuzzywuzzy') |
There was a problem hiding this comment.
We should add this back where we import fuzzywuzzy. I remember it spamming a lot.
There was a problem hiding this comment.
I see you've removed fuzzywuzzy, ok too 👍
| class ServiceIntentHandler(IntentHandler): | ||
| """Intent handler registration.""" | ||
|
|
||
| domain = None |
There was a problem hiding this comment.
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.
Do you mean by extending/creating a different async_register?
There was a problem hiding this comment.
class ServiceIntentHandler(IntentHandler):
def __init__(self, domain, service, response):
self.domain = domain
self.service = service
self.response = responseThere was a problem hiding this comment.
Well, when you put it that way :-) Thanks again.
| return [x for _, _, x in sorted(matches)] | ||
|
|
||
|
|
||
| class ServiceIntentHandler(IntentHandler): |
There was a problem hiding this comment.
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.
Updated. I think having the slot schema in the class also makes it clearer.
| 'pushbullet.py', | ||
| 'py-canary', | ||
| 'pydispatcher', | ||
| 'python-Levenshtein', |
|
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. |
| assert call.service == 'toggle' | ||
| assert call.data == {'entity_id': ['light.test_light']} | ||
|
|
||
| @asyncio.coroutine |
| assert call.service == 'turn_off' | ||
| assert call.data == {'entity_id': ['light.test_light']} | ||
|
|
||
| @asyncio.coroutine |
| assert call.service == 'turn_on' | ||
| assert call.data == {'entity_id': ['light.test_light']} | ||
|
|
||
| @asyncio.coroutine |
| assert mock_check.called | ||
| assert not mock_stop.called | ||
|
|
||
| @asyncio.coroutine |
| 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.
'homeassistant.components.group' imported but unused
| # pylint: disable=protected-access | ||
| import asyncio | ||
| import unittest | ||
| import inspect |
| assert call.service == 'toggle' | ||
| assert call.data == {'entity_id': ['light.test_light']} | ||
|
|
||
| @asyncio.coroutine |
| assert call.service == 'turn_off' | ||
| assert call.data == {'entity_id': ['light.test_light']} | ||
|
|
||
| @asyncio.coroutine |
| assert call.service == 'turn_on' | ||
| assert call.data == {'entity_id': ['light.test_light']} | ||
|
|
||
| @asyncio.coroutine |
| assert mock_check.called | ||
| assert not mock_stop.called | ||
|
|
||
| @asyncio.coroutine |
| 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.
'homeassistant.components.group' imported but unused
| # pylint: disable=protected-access | ||
| import asyncio | ||
| import unittest | ||
| import inspect |
| 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.
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.
We match against entity name and not id right?
There was a problem hiding this comment.
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.
| 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.
Please add a comment why this is? Also, it seems very English focused
There was a problem hiding this comment.
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.
This sounds like an issue that should be fixed by things launching intents?
There was a problem hiding this comment.
Yep, makes sense that it would be done by extending utterances instead.
| 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.
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.
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.
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.
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.
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.
The best filter would be to filter by entites that support the given domain.service
There was a problem hiding this comment.
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)
| yield from hass.services.async_call( | ||
| self.domain, self.service, { | ||
| ATTR_ENTITY_ID: entity_id | ||
| }, blocking=True) |
There was a problem hiding this comment.
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.
| 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.
Please remove period at the end.
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:
toxrun successfully. Your PR cannot be merged unless tests passREQUIREMENTSvariable (example).requirements_all.txtby runningscript/gen_requirements_all.py..coveragerc.If the code does not interact with devices:
toxrun successfully. Your PR cannot be merged unless tests pass