Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
eedb3d0
Moved TurnOn/Off Intents to component
todschmidt Feb 3, 2018
bf015c9
Removed unused import
todschmidt Feb 3, 2018
b3d7bf6
Lint fix which my local runs dont catch apparently...
todschmidt Feb 3, 2018
8651566
Moved hass intent code into intent
todschmidt Feb 4, 2018
c842be2
Added test for toggle to conversation.
todschmidt Feb 4, 2018
04b407a
Fixed toggle tests
todschmidt Feb 4, 2018
c479075
Update intent.py
todschmidt Feb 4, 2018
13d0d9a
Added homeassistant.helpers to gen_requirements script.
todschmidt Feb 5, 2018
ac5d2e5
Merge branch 'intent-registry-2' of github.com:tschmidty69/home-assis…
todschmidt Feb 5, 2018
2dcfe18
Update intent.py
todschmidt Feb 5, 2018
861cdc5
Update intent.py
todschmidt Feb 5, 2018
2214096
Changed return value for _match_entity
todschmidt Feb 5, 2018
90280d7
Moved consts and requirements
todschmidt Feb 6, 2018
f287689
Removed unused import
todschmidt Feb 6, 2018
7992a73
Removed http view
todschmidt Feb 6, 2018
5e987e8
Removed http import
todschmidt Feb 6, 2018
5496efd
Removed fuzzywuzzy dependency
todschmidt Feb 7, 2018
cb8ab73
woof
todschmidt Feb 7, 2018
2d9becd
A few cleanups
todschmidt Feb 8, 2018
0d955d3
Added domain filtering to entities
todschmidt Feb 8, 2018
1262ba3
Clarified class doc string
todschmidt Feb 8, 2018
ed1bcf2
Added doc string
todschmidt Feb 8, 2018
6cfce83
Added test in test_init
todschmidt Feb 9, 2018
95d30c6
woof
todschmidt Feb 9, 2018
42273fc
Cleanup entity matching
todschmidt Feb 10, 2018
a43bc92
Update intent.py
todschmidt Feb 10, 2018
b2ad2f9
removed uneeded setup from tests
todschmidt Feb 11, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion homeassistant/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
import homeassistant.core as ha
import homeassistant.config as conf_util
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import extract_entity_ids
from homeassistant.helpers import intent
from homeassistant.helpers.intent import (INTENT_TURN_ON,

Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Complains without it

Feb  7 09:03:14 home hass[25233]:     async_register(hass, INTENT_TURN_ON,
Feb  7 09:03:14 home hass[25233]: NameError: name 'INTENT_TURN_ON' is not defined#033[0m

Copy link
Copy Markdown
Member

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_ON

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Well, of course :-). Thanks.

INTENT_TURN_OFF, INTENT_TOGGLE)
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART,
Expand All @@ -25,7 +29,6 @@
SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config'
SERVICE_CHECK_CONFIG = 'check_config'


def is_on(hass, entity_id=None):
"""Load up the module to call the is_on method.

Expand Down Expand Up @@ -154,6 +157,10 @@ def async_handle_turn_service(service):
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service)
hass.services.async_register(
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
hass.helpers.intent.async_register(TurnOnIntent())
hass.helpers.intent.async_register(TurnOffIntent())
hass.helpers.intent.async_register(ToggleIntent())


@asyncio.coroutine
def async_handle_core_service(call):
Expand Down Expand Up @@ -200,3 +207,39 @@ def async_handle_reload_config(call):
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config)

return True


class TurnOnIntent(intent.ServiceIntentHandler):
"""Handle component turn intents."""

intent_type = INTENT_TURN_ON
slot_schema = {
'name': cv.string,
}
domain = ha.DOMAIN
service = SERVICE_TURN_ON
response = "Turned on {}"


class TurnOffIntent(intent.ServiceIntentHandler):
"""Handle component turn intents."""

intent_type = INTENT_TURN_OFF
slot_schema = {
'name': cv.string,
}
domain = ha.DOMAIN
service = SERVICE_TURN_OFF
response = "Turned off {}"


class ToggleIntent(intent.ServiceIntentHandler):
"""Handle component turn intents."""

intent_type = INTENT_TOGGLE
slot_schema = {
'name': cv.string,
}
domain = ha.DOMAIN
service = SERVICE_TOGGLE
response = "Toggled {}"
95 changes: 7 additions & 88 deletions homeassistant/components/conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(''))

Expand All @@ -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.
Expand All @@ -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
Copy Markdown
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
Copy Markdown
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 👍


config = config.get(DOMAIN, {})
intents = hass.data.get(DOMAIN)

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

Expand Down Expand Up @@ -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."""

Expand Down
67 changes: 64 additions & 3 deletions homeassistant/helpers/intent.py
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'

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -117,6 +124,60 @@ def __repr__(self):
return '<{} - {}>'.format(self.__class__.__name__, self.intent_type)


def fuzzyfinder(name, entities):

Copy link
Copy Markdown

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

"""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):

Copy link
Copy Markdown
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
Copy Markdown
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.

"""Intent handler registration."""

domain = None

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

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()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why ?

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

entity_name = entity_name.replace('the_', '')

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.


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)

Copy link
Copy Markdown
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.


response.async_set_speech(
self.response.format(name))
return response


class Intent:
"""Hold the intent."""

Expand Down
3 changes: 0 additions & 3 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -294,9 +294,6 @@ fritzhome==1.0.4
# homeassistant.components.media_player.frontier_silicon
fsapi==0.0.7

# homeassistant.components.conversation
fuzzywuzzy==0.16.0

# homeassistant.components.tts.google
gTTS-token==1.1.1

Expand Down
3 changes: 0 additions & 3 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,6 @@ evohomeclient==0.2.5
# homeassistant.components.sensor.geo_rss_events
feedparser==5.2.1

# homeassistant.components.conversation
fuzzywuzzy==0.16.0

# homeassistant.components.tts.google
gTTS-token==1.1.1

Expand Down
1 change: 1 addition & 0 deletions script/gen_requirements_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
'pushbullet.py',
'py-canary',
'pydispatcher',
'python-Levenshtein',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No longer needed

'PyJWT',
'pylitejet',
'pymonoprice',
Expand Down
Loading