Skip to content
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

Merged
merged 27 commits into from
Feb 11, 2018

Conversation

tschmidty69
Copy link
Contributor

@tschmidty69 tschmidty69 commented Feb 4, 2018

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:

  • The code change is tested and works locally.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • Local tests with tox run successfully. Your PR cannot be merged unless tests pass
  • New dependencies have been added to the REQUIREMENTS variable (example).
  • New dependencies are only imported inside functions that use them (example).
  • New dependencies have been added to requirements_all.txt by running script/gen_requirements_all.py.
  • New files were added to .coveragerc.

If the code does not interact with devices:

  • Local tests with tox run successfully. Your PR cannot be merged unless tests pass
  • Tests have been added to verify that the new code works.

@@ -80,6 +84,17 @@ class IntentHandleError(IntentError):

pass

@callback

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

@tschmidty69 tschmidty69 changed the title Move HassIntent handler code into helpers/intent [WIP] Move HassIntent handler code into helpers/intent Feb 4, 2018
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
name = slots['name']['value']
entity = _match_entity(hass, name)
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

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

Copy link
Member

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.

@@ -165,6 +165,11 @@
EVENT_LOGBOOK_ENTRY = 'logbook_entry'
EVENT_THEMES_UPDATED = 'themes_updated'

# #### INTENTS ####
Copy link
Member

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 .

Copy link
Contributor Author

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.


REQUIREMENTS = ['fuzzywuzzy==0.16.0', 'python-Levenshtein==0.12.0']
Copy link
Member

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.

Copy link
Contributor Author

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.

@@ -2,19 +2,26 @@
import asyncio
import logging

import fuzzywuzzy

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'fuzzywuzzy' imported but unused

@tschmidty69
Copy link
Contributor Author

@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')
Copy link
Member

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.

Copy link
Member

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 👍

class ServiceIntentHandler(IntentHandler):
"""Intent handler registration."""

domain = None
Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

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

Copy link
Contributor Author

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.

return [x for _, _, x in sorted(matches)]


class ServiceIntentHandler(IntentHandler):
Copy link
Member

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

Copy link
Contributor Author

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.

@@ -66,6 +66,7 @@
'pushbullet.py',
'py-canary',
'pydispatcher',
'python-Levenshtein',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer needed

@tschmidty69 tschmidty69 changed the title Move HassIntent handler code into helpers/intent [WIP] Move HassIntent handler code into helpers/intent Feb 8, 2018
@tschmidty69
Copy link
Contributor Author

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

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

assert call.service == 'turn_off'
assert call.data == {'entity_id': ['light.test_light']}

@asyncio.coroutine

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

assert call.service == 'turn_on'
assert call.data == {'entity_id': ['light.test_light']}

@asyncio.coroutine

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

@@ -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

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

@@ -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

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

@@ -2,6 +2,7 @@
# pylint: disable=protected-access
import asyncio
import unittest
import inspect

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'inspect' imported but unused

assert call.service == 'toggle'
assert call.data == {'entity_id': ['light.test_light']}

@asyncio.coroutine

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

assert call.service == 'turn_off'
assert call.data == {'entity_id': ['light.test_light']}

@asyncio.coroutine

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

assert call.service == 'turn_on'
assert call.data == {'entity_id': ['light.test_light']}

@asyncio.coroutine

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

@@ -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

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

@@ -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

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

@@ -2,6 +2,7 @@
# pylint: disable=protected-access
import asyncio
import unittest
import inspect

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'inspect' imported but unused

@tschmidty69 tschmidty69 changed the title [WIP] Move HassIntent handler code into helpers/intent Move HassIntent handler code into helpers/intent Feb 9, 2018
name = slots['name']['value']
entities = {state.entity_id: state.name for state
in hass.states.async_all()}
entity_name = name.replace(' ', '_').lower()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why ?

Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Contributor Author

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.

entities = {state.entity_id: state.name for state
in hass.states.async_all()}
entity_name = name.replace(' ', '_').lower()
entity_name = entity_name.replace('the_', '')
Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Contributor Author

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.

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)
Copy link
Member

@balloob balloob Feb 9, 2018

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?

Copy link
Contributor Author

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.

Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Member

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)

yield from hass.services.async_call(
self.domain, self.service, {
ATTR_ENTITY_ID: entity_id
}, blocking=True)
Copy link
Member

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.

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)
Copy link
Member

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.

Copy link
Member

@balloob balloob left a 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.

@balloob balloob merged commit 26209de into home-assistant:dev Feb 11, 2018
@balloob balloob mentioned this pull request Feb 22, 2018
@home-assistant home-assistant locked and limited conversation to collaborators May 29, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

RFC: Adding Default Hass Intents to Components
5 participants