Added HassOpenCover and HassCloseCover intents (#13372)

* Added intents to cover

* Added test for cover intents

* Style fixes

* Reverted reversions

* Async fixes

* Woof

* Added conditional loading

* Added conditional loading

* Added conditional loading

* Moved tests, fixed logic

* Moved tests, fixed logic

* Pylint

* Pylint

* Refactored componenet registration

* Refactored componenet registration

* Lint
This commit is contained in:
Tod Schmidt 2018-03-30 20:22:48 -04:00 committed by Paulus Schoutsen
parent bf58945680
commit bf44dc422c
5 changed files with 185 additions and 83 deletions

View File

@ -13,10 +13,14 @@ from homeassistant import core
from homeassistant.components import http
from homeassistant.components.http.data_validator import (
RequestDataValidator)
from homeassistant.components.cover import (INTENT_OPEN_COVER,
INTENT_CLOSE_COVER)
from homeassistant.const import EVENT_COMPONENT_LOADED
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import intent
from homeassistant.loader import bind_hass
from homeassistant.setup import (ATTR_COMPONENT)
_LOGGER = logging.getLogger(__name__)
@ -28,6 +32,13 @@ DOMAIN = 'conversation'
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
REGEX_TYPE = type(re.compile(''))
UTTERANCES = {
'cover': {
INTENT_OPEN_COVER: ['Open [the] [a] [an] {name}[s]'],
INTENT_CLOSE_COVER: ['Close [the] [a] [an] {name}[s]']
}
}
SERVICE_PROCESS = 'process'
SERVICE_PROCESS_SCHEMA = vol.Schema({
@ -112,6 +123,25 @@ async def async_setup(hass, config):
'[the] [a] [an] {name}[s] toggle',
])
@callback
def register_utterances(component):
"""Register utterances for a component."""
if component not in UTTERANCES:
return
for intent_type, sentences in UTTERANCES[component].items():
async_register(hass, intent_type, sentences)
@callback
def component_loaded(event):
"""Handle a new component loaded."""
register_utterances(event.data[ATTR_COMPONENT])
hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded)
# Check already loaded components.
for component in hass.config.components:
register_utterances(component)
return True

View File

@ -17,6 +17,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.components import group
from homeassistant.helpers import intent
from homeassistant.const import (
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION,
SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
@ -55,6 +56,9 @@ ATTR_CURRENT_TILT_POSITION = 'current_tilt_position'
ATTR_POSITION = 'position'
ATTR_TILT_POSITION = 'tilt_position'
INTENT_OPEN_COVER = 'HassOpenCover'
INTENT_CLOSE_COVER = 'HassCloseCover'
COVER_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
@ -181,6 +185,12 @@ async def async_setup(hass, config):
hass.services.async_register(
DOMAIN, service_name, async_handle_cover_service,
schema=schema)
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER,
"Opened {}"))
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER,
"Closed {}"))
return True

View File

@ -0,0 +1,49 @@
"""The tests for the cover platform."""
from homeassistant.components.cover import (SERVICE_OPEN_COVER,
SERVICE_CLOSE_COVER)
from homeassistant.components import intent
import homeassistant.components as comps
from tests.common import async_mock_service
async def test_open_cover_intent(hass):
"""Test HassOpenCover intent."""
result = await comps.cover.async_setup(hass, {})
assert result
hass.states.async_set('cover.garage_door', 'closed')
calls = async_mock_service(hass, 'cover', SERVICE_OPEN_COVER)
response = await intent.async_handle(
hass, 'test', 'HassOpenCover', {'name': {'value': 'garage door'}}
)
await hass.async_block_till_done()
assert response.speech['plain']['speech'] == 'Opened garage door'
assert len(calls) == 1
call = calls[0]
assert call.domain == 'cover'
assert call.service == 'open_cover'
assert call.data == {'entity_id': 'cover.garage_door'}
async def test_close_cover_intent(hass):
"""Test HassCloseCover intent."""
result = await comps.cover.async_setup(hass, {})
assert result
hass.states.async_set('cover.garage_door', 'open')
calls = async_mock_service(hass, 'cover', SERVICE_CLOSE_COVER)
response = await intent.async_handle(
hass, 'test', 'HassCloseCover', {'name': {'value': 'garage door'}}
)
await hass.async_block_till_done()
assert response.speech['plain']['speech'] == 'Closed garage door'
assert len(calls) == 1
call = calls[0]
assert call.domain == 'cover'
assert call.service == 'close_cover'
assert call.data == {'entity_id': 'cover.garage_door'}

View File

@ -1,26 +1,24 @@
"""The tests for the Conversation component."""
# pylint: disable=protected-access
import asyncio
import pytest
from homeassistant.setup import async_setup_component
from homeassistant.components import conversation
import homeassistant.components as component
from homeassistant.components.cover import (SERVICE_OPEN_COVER)
from homeassistant.helpers import intent
from tests.common import async_mock_intent, async_mock_service
@asyncio.coroutine
def test_calling_intent(hass):
async def test_calling_intent(hass):
"""Test calling an intent from a conversation."""
intents = async_mock_intent(hass, 'OrderBeer')
result = yield from component.async_setup(hass, {})
result = await component.async_setup(hass, {})
assert result
result = yield from async_setup_component(hass, 'conversation', {
result = await async_setup_component(hass, 'conversation', {
'conversation': {
'intents': {
'OrderBeer': [
@ -31,11 +29,11 @@ def test_calling_intent(hass):
})
assert result
yield from hass.services.async_call(
await hass.services.async_call(
'conversation', 'process', {
conversation.ATTR_TEXT: 'I would like the Grolsch beer'
})
yield from hass.async_block_till_done()
await hass.async_block_till_done()
assert len(intents) == 1
intent = intents[0]
@ -45,8 +43,7 @@ def test_calling_intent(hass):
assert intent.text_input == 'I would like the Grolsch beer'
@asyncio.coroutine
def test_register_before_setup(hass):
async def test_register_before_setup(hass):
"""Test calling an intent from a conversation."""
intents = async_mock_intent(hass, 'OrderBeer')
@ -54,7 +51,7 @@ def test_register_before_setup(hass):
'A {type} beer, please'
])
result = yield from async_setup_component(hass, 'conversation', {
result = await async_setup_component(hass, 'conversation', {
'conversation': {
'intents': {
'OrderBeer': [
@ -65,11 +62,11 @@ def test_register_before_setup(hass):
})
assert result
yield from hass.services.async_call(
await hass.services.async_call(
'conversation', 'process', {
conversation.ATTR_TEXT: 'A Grolsch beer, please'
})
yield from hass.async_block_till_done()
await hass.async_block_till_done()
assert len(intents) == 1
intent = intents[0]
@ -78,11 +75,11 @@ def test_register_before_setup(hass):
assert intent.slots == {'type': {'value': 'Grolsch'}}
assert intent.text_input == 'A Grolsch beer, please'
yield from hass.services.async_call(
await hass.services.async_call(
'conversation', 'process', {
conversation.ATTR_TEXT: 'I would like the Grolsch beer'
})
yield from hass.async_block_till_done()
await hass.async_block_till_done()
assert len(intents) == 2
intent = intents[1]
@ -92,14 +89,14 @@ def test_register_before_setup(hass):
assert intent.text_input == 'I would like the Grolsch beer'
@asyncio.coroutine
def test_http_processing_intent(hass, aiohttp_client):
async def test_http_processing_intent(hass, test_client):
"""Test processing intent via HTTP API."""
class TestIntentHandler(intent.IntentHandler):
"""Test Intent Handler."""
intent_type = 'OrderBeer'
@asyncio.coroutine
def async_handle(self, intent):
async def async_handle(self, intent):
"""Handle the intent."""
response = intent.create_response()
response.async_set_speech(
@ -111,7 +108,7 @@ def test_http_processing_intent(hass, aiohttp_client):
intent.async_register(hass, TestIntentHandler())
result = yield from async_setup_component(hass, 'conversation', {
result = await async_setup_component(hass, 'conversation', {
'conversation': {
'intents': {
'OrderBeer': [
@ -122,13 +119,13 @@ def test_http_processing_intent(hass, aiohttp_client):
})
assert result
client = yield from aiohttp_client(hass.http.app)
resp = yield from client.post('/api/conversation/process', json={
client = await test_client(hass.http.app)
resp = await client.post('/api/conversation/process', json={
'text': 'I would like the Grolsch beer'
})
assert resp.status == 200
data = yield from resp.json()
data = await resp.json()
assert data == {
'card': {
@ -145,24 +142,23 @@ def test_http_processing_intent(hass, aiohttp_client):
}
@asyncio.coroutine
@pytest.mark.parametrize('sentence', ('turn on kitchen', 'turn kitchen on'))
def test_turn_on_intent(hass, sentence):
async def test_turn_on_intent(hass, sentence):
"""Test calling the turn on intent."""
result = yield from component.async_setup(hass, {})
result = await component.async_setup(hass, {})
assert result
result = yield from async_setup_component(hass, 'conversation', {})
result = await async_setup_component(hass, 'conversation', {})
assert result
hass.states.async_set('light.kitchen', 'off')
calls = async_mock_service(hass, 'homeassistant', 'turn_on')
yield from hass.services.async_call(
await hass.services.async_call(
'conversation', 'process', {
conversation.ATTR_TEXT: sentence
})
yield from hass.async_block_till_done()
await hass.async_block_till_done()
assert len(calls) == 1
call = calls[0]
@ -171,24 +167,49 @@ def test_turn_on_intent(hass, sentence):
assert call.data == {'entity_id': 'light.kitchen'}
@asyncio.coroutine
@pytest.mark.parametrize('sentence', ('turn off kitchen', 'turn kitchen off'))
def test_turn_off_intent(hass, sentence):
"""Test calling the turn on intent."""
result = yield from component.async_setup(hass, {})
async def test_cover_intents_loading(hass):
"""Test Cover Intents Loading."""
with pytest.raises(intent.UnknownIntent):
await intent.async_handle(
hass, 'test', 'HassOpenCover', {'name': {'value': 'garage door'}}
)
result = await async_setup_component(hass, 'cover', {})
assert result
result = yield from async_setup_component(hass, 'conversation', {})
hass.states.async_set('cover.garage_door', 'closed')
calls = async_mock_service(hass, 'cover', SERVICE_OPEN_COVER)
response = await intent.async_handle(
hass, 'test', 'HassOpenCover', {'name': {'value': 'garage door'}}
)
await hass.async_block_till_done()
assert response.speech['plain']['speech'] == 'Opened garage door'
assert len(calls) == 1
call = calls[0]
assert call.domain == 'cover'
assert call.service == 'open_cover'
assert call.data == {'entity_id': 'cover.garage_door'}
@pytest.mark.parametrize('sentence', ('turn off kitchen', 'turn kitchen off'))
async def test_turn_off_intent(hass, sentence):
"""Test calling the turn on intent."""
result = await component.async_setup(hass, {})
assert result
result = await async_setup_component(hass, 'conversation', {})
assert result
hass.states.async_set('light.kitchen', 'on')
calls = async_mock_service(hass, 'homeassistant', 'turn_off')
yield from hass.services.async_call(
await hass.services.async_call(
'conversation', 'process', {
conversation.ATTR_TEXT: sentence
})
yield from hass.async_block_till_done()
await hass.async_block_till_done()
assert len(calls) == 1
call = calls[0]
@ -197,24 +218,23 @@ def test_turn_off_intent(hass, sentence):
assert call.data == {'entity_id': 'light.kitchen'}
@asyncio.coroutine
@pytest.mark.parametrize('sentence', ('toggle kitchen', 'kitchen toggle'))
def test_toggle_intent(hass, sentence):
async def test_toggle_intent(hass, sentence):
"""Test calling the turn on intent."""
result = yield from component.async_setup(hass, {})
result = await component.async_setup(hass, {})
assert result
result = yield from async_setup_component(hass, 'conversation', {})
result = await async_setup_component(hass, 'conversation', {})
assert result
hass.states.async_set('light.kitchen', 'on')
calls = async_mock_service(hass, 'homeassistant', 'toggle')
yield from hass.services.async_call(
await hass.services.async_call(
'conversation', 'process', {
conversation.ATTR_TEXT: sentence
})
yield from hass.async_block_till_done()
await hass.async_block_till_done()
assert len(calls) == 1
call = calls[0]
@ -223,20 +243,19 @@ def test_toggle_intent(hass, sentence):
assert call.data == {'entity_id': 'light.kitchen'}
@asyncio.coroutine
def test_http_api(hass, aiohttp_client):
async def test_http_api(hass, test_client):
"""Test the HTTP conversation API."""
result = yield from component.async_setup(hass, {})
result = await component.async_setup(hass, {})
assert result
result = yield from async_setup_component(hass, 'conversation', {})
result = await async_setup_component(hass, 'conversation', {})
assert result
client = yield from aiohttp_client(hass.http.app)
client = await test_client(hass.http.app)
hass.states.async_set('light.kitchen', 'off')
calls = async_mock_service(hass, 'homeassistant', 'turn_on')
resp = yield from client.post('/api/conversation/process', json={
resp = await client.post('/api/conversation/process', json={
'text': 'Turn the kitchen on'
})
assert resp.status == 200
@ -248,23 +267,22 @@ def test_http_api(hass, aiohttp_client):
assert call.data == {'entity_id': 'light.kitchen'}
@asyncio.coroutine
def test_http_api_wrong_data(hass, aiohttp_client):
async def test_http_api_wrong_data(hass, test_client):
"""Test the HTTP conversation API."""
result = yield from component.async_setup(hass, {})
result = await component.async_setup(hass, {})
assert result
result = yield from async_setup_component(hass, 'conversation', {})
result = await async_setup_component(hass, 'conversation', {})
assert result
client = yield from aiohttp_client(hass.http.app)
client = await test_client(hass.http.app)
resp = yield from client.post('/api/conversation/process', json={
resp = await client.post('/api/conversation/process', json={
'text': 123
})
assert resp.status == 400
resp = yield from client.post('/api/conversation/process', json={
resp = await client.post('/api/conversation/process', json={
})
assert resp.status == 400

View File

@ -1,6 +1,5 @@
"""The tests for Core components."""
# pylint: disable=protected-access
import asyncio
import unittest
from unittest.mock import patch, Mock
@ -75,9 +74,9 @@ class TestComponentsCore(unittest.TestCase):
self.hass.block_till_done()
self.assertEqual(1, len(calls))
@asyncio.coroutine
@patch('homeassistant.core.ServiceRegistry.call')
def test_turn_on_to_not_block_for_domains_without_service(self, mock_call):
async def test_turn_on_to_not_block_for_domains_without_service(self,
mock_call):
"""Test if turn_on is blocking domain with no service."""
async_mock_service(self.hass, 'light', SERVICE_TURN_ON)
@ -88,7 +87,7 @@ class TestComponentsCore(unittest.TestCase):
'entity_id': ['light.test', 'sensor.bla', 'light.bla']
})
service = self.hass.services._services['homeassistant']['turn_on']
yield from service.func(service_call)
await service.func(service_call)
self.assertEqual(2, mock_call.call_count)
self.assertEqual(
@ -130,8 +129,8 @@ class TestComponentsCore(unittest.TestCase):
comps.reload_core_config(self.hass)
self.hass.block_till_done()
assert 10 == self.hass.config.latitude
assert 20 == self.hass.config.longitude
assert self.hass.config.latitude == 10
assert self.hass.config.longitude == 20
ent.schedule_update_ha_state()
self.hass.block_till_done()
@ -198,19 +197,18 @@ class TestComponentsCore(unittest.TestCase):
assert not mock_stop.called
@asyncio.coroutine
def test_turn_on_intent(hass):
async def test_turn_on_intent(hass):
"""Test HassTurnOn intent."""
result = yield from comps.async_setup(hass, {})
result = await comps.async_setup(hass, {})
assert result
hass.states.async_set('light.test_light', 'off')
calls = async_mock_service(hass, 'light', SERVICE_TURN_ON)
response = yield from intent.async_handle(
response = await intent.async_handle(
hass, 'test', 'HassTurnOn', {'name': {'value': 'test light'}}
)
yield from hass.async_block_till_done()
await hass.async_block_till_done()
assert response.speech['plain']['speech'] == 'Turned test light on'
assert len(calls) == 1
@ -220,19 +218,18 @@ def test_turn_on_intent(hass):
assert call.data == {'entity_id': ['light.test_light']}
@asyncio.coroutine
def test_turn_off_intent(hass):
async def test_turn_off_intent(hass):
"""Test HassTurnOff intent."""
result = yield from comps.async_setup(hass, {})
result = await comps.async_setup(hass, {})
assert result
hass.states.async_set('light.test_light', 'on')
calls = async_mock_service(hass, 'light', SERVICE_TURN_OFF)
response = yield from intent.async_handle(
response = await intent.async_handle(
hass, 'test', 'HassTurnOff', {'name': {'value': 'test light'}}
)
yield from hass.async_block_till_done()
await hass.async_block_till_done()
assert response.speech['plain']['speech'] == 'Turned test light off'
assert len(calls) == 1
@ -242,19 +239,18 @@ def test_turn_off_intent(hass):
assert call.data == {'entity_id': ['light.test_light']}
@asyncio.coroutine
def test_toggle_intent(hass):
async def test_toggle_intent(hass):
"""Test HassToggle intent."""
result = yield from comps.async_setup(hass, {})
result = await comps.async_setup(hass, {})
assert result
hass.states.async_set('light.test_light', 'off')
calls = async_mock_service(hass, 'light', SERVICE_TOGGLE)
response = yield from intent.async_handle(
response = await intent.async_handle(
hass, 'test', 'HassToggle', {'name': {'value': 'test light'}}
)
yield from hass.async_block_till_done()
await hass.async_block_till_done()
assert response.speech['plain']['speech'] == 'Toggled test light'
assert len(calls) == 1
@ -264,13 +260,12 @@ def test_toggle_intent(hass):
assert call.data == {'entity_id': ['light.test_light']}
@asyncio.coroutine
def test_turn_on_multiple_intent(hass):
async def test_turn_on_multiple_intent(hass):
"""Test HassTurnOn intent with multiple similar entities.
This tests that matching finds the proper entity among similar names.
"""
result = yield from comps.async_setup(hass, {})
result = await comps.async_setup(hass, {})
assert result
hass.states.async_set('light.test_light', 'off')
@ -278,10 +273,10 @@ def test_turn_on_multiple_intent(hass):
hass.states.async_set('light.test_lighter', 'off')
calls = async_mock_service(hass, 'light', SERVICE_TURN_ON)
response = yield from intent.async_handle(
response = await intent.async_handle(
hass, 'test', 'HassTurnOn', {'name': {'value': 'test lights'}}
)
yield from hass.async_block_till_done()
await hass.async_block_till_done()
assert response.speech['plain']['speech'] == 'Turned test lights 2 on'
assert len(calls) == 1