diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py index e96694ce0a3e1..ddd96c99177d7 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation.py @@ -13,10 +13,14 @@ 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 @@ REGEX_TURN_COMMAND = re.compile(r'turn (?P(?: |\w)+) (?P\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 process(service): '[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 diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index b24361d82930d..e4c8f5634cf4a 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -17,6 +17,7 @@ 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_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_handle_cover_service(service): 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 diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py new file mode 100755 index 0000000000000..5df492d3d4714 --- /dev/null +++ b/tests/components/cover/test_init.py @@ -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'} diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py index bde00e10928c0..d9c29cdae837c 100644 --- a/tests/components/test_conversation.py +++ b/tests/components/test_conversation.py @@ -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 async_handle(self, intent): 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 async_handle(self, intent): }) 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 async_handle(self, intent): } -@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 +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 + + 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')) -def test_turn_off_intent(hass, sentence): +async def test_turn_off_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', '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 diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 991982af9b2bf..c8c7e0d809b5a 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -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 @@ def test_toggle(self): 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 @@ def test_turn_on_to_not_block_for_domains_without_service(self, mock_call): '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 @@ def test_reload_core_conf(self): 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 @@ def test_check_config(self, mock_check, mock_stop): 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