From ae650143a9a103e0ce067fe7f3386980f5be797b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 30 Nov 2019 23:36:38 -0800 Subject: [PATCH 1/2] Move intent registration to own integration. --- .../components/conversation/default_agent.py | 12 ++- homeassistant/components/cover/__init__.py | 14 --- homeassistant/components/cover/intent.py | 22 +++++ homeassistant/components/intent/__init__.py | 33 +++++++ homeassistant/components/light/__init__.py | 64 -------------- homeassistant/components/light/intent.py | 84 ++++++++++++++++++ .../components/shopping_list/__init__.py | 49 ----------- .../components/shopping_list/intent.py | 55 ++++++++++++ tests/components/conversation/test_init.py | 28 ------ .../cover/{test_init.py => test_intent.py} | 13 +-- tests/components/intent/test_init.py | 32 +++++++ tests/components/light/test_init.py | 84 ------------------ tests/components/light/test_intent.py | 88 +++++++++++++++++++ tests/components/shopping_list/conftest.py | 23 +++++ tests/components/shopping_list/test_init.py | 55 +++--------- tests/components/shopping_list/test_intent.py | 22 +++++ 16 files changed, 389 insertions(+), 289 deletions(-) create mode 100644 homeassistant/components/cover/intent.py create mode 100644 homeassistant/components/light/intent.py create mode 100644 homeassistant/components/shopping_list/intent.py rename tests/components/cover/{test_init.py => test_intent.py} (83%) create mode 100644 tests/components/light/test_intent.py create mode 100644 tests/components/shopping_list/conftest.py create mode 100644 tests/components/shopping_list/test_intent.py diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index e562eed7e666a1..2f09cba2eb196b 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -3,9 +3,12 @@ import re from typing import Optional -from homeassistant import core -from homeassistant.components.cover import INTENT_CLOSE_COVER, INTENT_OPEN_COVER -from homeassistant.components.shopping_list import INTENT_ADD_ITEM, INTENT_LAST_ITEMS +from homeassistant import core, setup +from homeassistant.components.cover.intent import INTENT_CLOSE_COVER, INTENT_OPEN_COVER +from homeassistant.components.shopping_list.intent import ( + INTENT_ADD_ITEM, + INTENT_LAST_ITEMS, +) from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.core import callback from homeassistant.helpers import intent @@ -58,6 +61,9 @@ def __init__(self, hass: core.HomeAssistant): async def async_initialize(self, config): """Initialize the default agent.""" + if "intent" not in self.hass.config.components: + await setup.async_setup_component(self.hass, "intent", {}) + config = config.get(DOMAIN, {}) intents = self.hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index d57bf678a69717..a3c28a77cbe64a 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -15,7 +15,6 @@ ) from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.components import group -from homeassistant.helpers import intent from homeassistant.const import ( SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, @@ -83,8 +82,6 @@ ATTR_POSITION = "position" ATTR_TILT_POSITION = "tilt_position" -INTENT_OPEN_COVER = "HassOpenCover" -INTENT_CLOSE_COVER = "HassCloseCover" COVER_SET_COVER_POSITION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( {vol.Required(ATTR_POSITION): vol.All(vol.Coerce(int), vol.Range(min=0, max=100))} @@ -158,17 +155,6 @@ async def async_setup(hass, config): SERVICE_TOGGLE_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_toggle_tilt" ) - 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/homeassistant/components/cover/intent.py b/homeassistant/components/cover/intent.py new file mode 100644 index 00000000000000..f8d13e6a90ebe5 --- /dev/null +++ b/homeassistant/components/cover/intent.py @@ -0,0 +1,22 @@ +"""Intents for the cover integration.""" +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent + +from . import DOMAIN, SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER + +INTENT_OPEN_COVER = "HassOpenCover" +INTENT_CLOSE_COVER = "HassCloseCover" + + +async def async_setup_intents(hass: HomeAssistant) -> None: + """Set up the cover intents.""" + 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 {}" + ) + ) diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index 758b77ba108d27..31ab36ecc89f65 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -1,22 +1,55 @@ """The Intent integration.""" +import asyncio +import logging + import voluptuous as vol from homeassistant.core import HomeAssistant +from homeassistant.const import EVENT_COMPONENT_LOADED +from homeassistant.setup import ATTR_COMPONENT from homeassistant.components import http from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.helpers import config_validation as cv, intent +from homeassistant.loader import async_get_integration, IntegrationNotFound from .const import DOMAIN CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) +_LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: dict): """Set up the Intent component.""" hass.http.register_view(IntentHandleView()) + + tasks = [_async_process_intent(hass, comp) for comp in hass.config.components] + + async def async_component_loaded(event): + """Handle a new component loaded.""" + await _async_process_intent(hass, event.data[ATTR_COMPONENT]) + + hass.bus.async_listen(EVENT_COMPONENT_LOADED, async_component_loaded) + + if tasks: + await asyncio.gather(*tasks) + return True +async def _async_process_intent(hass: HomeAssistant, component_name: str): + """Process the intents of a component.""" + try: + integration = await async_get_integration(hass, component_name) + platform = integration.get_platform(DOMAIN) + except (IntegrationNotFound, ImportError): + return + + try: + await platform.async_setup_intents(hass) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error setting up intents for %s", component_name) + + class IntentHandleView(http.HomeAssistantView): """View to handle intents from JSON.""" diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index b33cb29421e21a..0d60f3f6b52337 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -26,7 +26,6 @@ ) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers import intent from homeassistant.loader import bind_hass import homeassistant.util.color as color_util @@ -141,8 +140,6 @@ vol.ExactSequence((str, cv.small_float, cv.small_float, cv.byte)) ) -INTENT_SET = "HassLightSet" - _LOGGER = logging.getLogger(__name__) @@ -196,65 +193,6 @@ def preprocess_turn_off(params): return (False, None) # Light should be turned on -class SetIntentHandler(intent.IntentHandler): - """Handle set color intents.""" - - intent_type = INTENT_SET - slot_schema = { - vol.Required("name"): cv.string, - vol.Optional("color"): color_util.color_name_to_rgb, - vol.Optional("brightness"): vol.All(vol.Coerce(int), vol.Range(0, 100)), - } - - async def async_handle(self, intent_obj): - """Handle the hass intent.""" - hass = intent_obj.hass - slots = self.async_validate_slots(intent_obj.slots) - state = hass.helpers.intent.async_match_state( - slots["name"]["value"], - [state for state in hass.states.async_all() if state.domain == DOMAIN], - ) - - service_data = {ATTR_ENTITY_ID: state.entity_id} - speech_parts = [] - - if "color" in slots: - intent.async_test_feature(state, SUPPORT_COLOR, "changing colors") - service_data[ATTR_RGB_COLOR] = slots["color"]["value"] - # Use original passed in value of the color because we don't have - # human readable names for that internally. - speech_parts.append( - "the color {}".format(intent_obj.slots["color"]["value"]) - ) - - if "brightness" in slots: - intent.async_test_feature(state, SUPPORT_BRIGHTNESS, "changing brightness") - service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"] - speech_parts.append("{}% brightness".format(slots["brightness"]["value"])) - - await hass.services.async_call( - DOMAIN, SERVICE_TURN_ON, service_data, context=intent_obj.context - ) - - response = intent_obj.create_response() - - if not speech_parts: # No attributes changed - speech = f"Turned on {state.name}" - else: - parts = [f"Changed {state.name} to"] - for index, part in enumerate(speech_parts): - if index == 0: - parts.append(f" {part}") - elif index != len(speech_parts) - 1: - parts.append(f", {part}") - else: - parts.append(f" and {part}") - speech = "".join(parts) - - response.async_set_speech(speech) - return response - - async def async_setup(hass, config): """Expose light control via state machine and services.""" component = hass.data[DOMAIN] = EntityComponent( @@ -341,8 +279,6 @@ async def async_handle_light_on_service(service): SERVICE_TOGGLE, LIGHT_TOGGLE_SCHEMA, "async_toggle" ) - hass.helpers.intent.async_register(SetIntentHandler()) - return True diff --git a/homeassistant/components/light/intent.py b/homeassistant/components/light/intent.py new file mode 100644 index 00000000000000..b8d5e82cd6981e --- /dev/null +++ b/homeassistant/components/light/intent.py @@ -0,0 +1,84 @@ +"""Intents for the light integration.""" +from homeassistant.core import HomeAssistant +import voluptuous as vol + +from homeassistant.helpers import intent +import homeassistant.util.color as color_util +import homeassistant.helpers.config_validation as cv + +from . import ( + ATTR_ENTITY_ID, + SUPPORT_COLOR, + ATTR_RGB_COLOR, + ATTR_BRIGHTNESS_PCT, + SUPPORT_BRIGHTNESS, + DOMAIN, + SERVICE_TURN_ON, +) + + +INTENT_SET = "HassLightSet" + + +async def async_setup_intents(hass: HomeAssistant) -> None: + """Set up the light intents.""" + hass.helpers.intent.async_register(SetIntentHandler()) + + +class SetIntentHandler(intent.IntentHandler): + """Handle set color intents.""" + + intent_type = INTENT_SET + slot_schema = { + vol.Required("name"): cv.string, + vol.Optional("color"): color_util.color_name_to_rgb, + vol.Optional("brightness"): vol.All(vol.Coerce(int), vol.Range(0, 100)), + } + + async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: + """Handle the hass intent.""" + hass = intent_obj.hass + slots = self.async_validate_slots(intent_obj.slots) + state = hass.helpers.intent.async_match_state( + slots["name"]["value"], + [state for state in hass.states.async_all() if state.domain == DOMAIN], + ) + + service_data = {ATTR_ENTITY_ID: state.entity_id} + speech_parts = [] + + if "color" in slots: + intent.async_test_feature(state, SUPPORT_COLOR, "changing colors") + service_data[ATTR_RGB_COLOR] = slots["color"]["value"] + # Use original passed in value of the color because we don't have + # human readable names for that internally. + speech_parts.append( + "the color {}".format(intent_obj.slots["color"]["value"]) + ) + + if "brightness" in slots: + intent.async_test_feature(state, SUPPORT_BRIGHTNESS, "changing brightness") + service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"] + speech_parts.append("{}% brightness".format(slots["brightness"]["value"])) + + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, service_data, context=intent_obj.context + ) + + response = intent_obj.create_response() + + if not speech_parts: # No attributes changed + speech = f"Turned on {state.name}" + else: + parts = [f"Changed {state.name} to"] + for index, part in enumerate(speech_parts): + if index == 0: + parts.append(f" {part}") + elif index != len(speech_parts) - 1: + parts.append(f", {part}") + else: + parts.append(f" and {part}") + speech = "".join(parts) + + response.async_set_speech(speech) + return response diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index a5e901b8c6e7be..850b06332f8899 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -9,7 +9,6 @@ from homeassistant.core import callback from homeassistant.components import http from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.helpers import intent import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json from homeassistant.components import websocket_api @@ -20,8 +19,6 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({DOMAIN: {}}, extra=vol.ALLOW_EXTRA) EVENT = "shopping_list_updated" -INTENT_ADD_ITEM = "HassShoppingListAddItem" -INTENT_LAST_ITEMS = "HassShoppingListLastItems" ITEM_UPDATE_SCHEMA = vol.Schema({"complete": bool, ATTR_NAME: str}) PERSISTENCE = ".shopping_list.json" @@ -86,9 +83,6 @@ def complete_item_service(call): data = hass.data[DOMAIN] = ShoppingData(hass) yield from data.async_load() - intent.async_register(hass, AddItemIntent()) - intent.async_register(hass, ListTopItemsIntent()) - hass.services.async_register( DOMAIN, SERVICE_ADD_ITEM, add_item_service, schema=SERVICE_ITEM_SCHEMA ) @@ -175,49 +169,6 @@ def save(self): save_json(self.hass.config.path(PERSISTENCE), self.items) -class AddItemIntent(intent.IntentHandler): - """Handle AddItem intents.""" - - intent_type = INTENT_ADD_ITEM - slot_schema = {"item": cv.string} - - @asyncio.coroutine - def async_handle(self, intent_obj): - """Handle the intent.""" - slots = self.async_validate_slots(intent_obj.slots) - item = slots["item"]["value"] - intent_obj.hass.data[DOMAIN].async_add(item) - - response = intent_obj.create_response() - response.async_set_speech(f"I've added {item} to your shopping list") - intent_obj.hass.bus.async_fire(EVENT) - return response - - -class ListTopItemsIntent(intent.IntentHandler): - """Handle AddItem intents.""" - - intent_type = INTENT_LAST_ITEMS - slot_schema = {"item": cv.string} - - @asyncio.coroutine - def async_handle(self, intent_obj): - """Handle the intent.""" - items = intent_obj.hass.data[DOMAIN].items[-5:] - response = intent_obj.create_response() - - if not items: - response.async_set_speech("There are no items on your shopping list") - else: - response.async_set_speech( - "These are the top {} items on your shopping list: {}".format( - min(len(items), 5), - ", ".join(itm["name"] for itm in reversed(items)), - ) - ) - return response - - class ShoppingListView(http.HomeAssistantView): """View to retrieve shopping list content.""" diff --git a/homeassistant/components/shopping_list/intent.py b/homeassistant/components/shopping_list/intent.py new file mode 100644 index 00000000000000..21ae7181e895bf --- /dev/null +++ b/homeassistant/components/shopping_list/intent.py @@ -0,0 +1,55 @@ +"""Intents for the Shopping List integration.""" +from homeassistant.helpers import intent +import homeassistant.helpers.config_validation as cv + +from . import DOMAIN, EVENT + +INTENT_ADD_ITEM = "HassShoppingListAddItem" +INTENT_LAST_ITEMS = "HassShoppingListLastItems" + + +async def async_setup_intents(hass): + """Set up the Shopping List intents.""" + intent.async_register(hass, AddItemIntent()) + intent.async_register(hass, ListTopItemsIntent()) + + +class AddItemIntent(intent.IntentHandler): + """Handle AddItem intents.""" + + intent_type = INTENT_ADD_ITEM + slot_schema = {"item": cv.string} + + async def async_handle(self, intent_obj): + """Handle the intent.""" + slots = self.async_validate_slots(intent_obj.slots) + item = slots["item"]["value"] + intent_obj.hass.data[DOMAIN].async_add(item) + + response = intent_obj.create_response() + response.async_set_speech(f"I've added {item} to your shopping list") + intent_obj.hass.bus.async_fire(EVENT) + return response + + +class ListTopItemsIntent(intent.IntentHandler): + """Handle AddItem intents.""" + + intent_type = INTENT_LAST_ITEMS + slot_schema = {"item": cv.string} + + async def async_handle(self, intent_obj): + """Handle the intent.""" + items = intent_obj.hass.data[DOMAIN].items[-5:] + response = intent_obj.create_response() + + if not items: + response.async_set_speech("There are no items on your shopping list") + else: + response.async_set_speech( + "These are the top {} items on your shopping list: {}".format( + min(len(items), 5), + ", ".join(itm["name"] for itm in reversed(items)), + ) + ) + return response diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 6d318deacdd72d..45008ef9447600 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -1,11 +1,9 @@ """The tests for the Conversation component.""" -# pylint: disable=protected-access import pytest from homeassistant.core import DOMAIN as HASS_DOMAIN, Context from homeassistant.setup import async_setup_component from homeassistant.components import conversation -from homeassistant.components.cover import SERVICE_OPEN_COVER from homeassistant.helpers import intent from tests.common import async_mock_intent, async_mock_service @@ -153,32 +151,6 @@ async def test_turn_on_intent(hass, sentence): assert call.data == {"entity_id": "light.kitchen"} -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")) async def test_turn_off_intent(hass, sentence): """Test calling the turn on intent.""" diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_intent.py similarity index 83% rename from tests/components/cover/test_init.py rename to tests/components/cover/test_intent.py index d1ca17d18f3525..ce01e882941bde 100644 --- a/tests/components/cover/test_init.py +++ b/tests/components/cover/test_intent.py @@ -1,15 +1,17 @@ """The tests for the cover platform.""" -from homeassistant.components.cover import SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER +from homeassistant.components.cover import ( + SERVICE_OPEN_COVER, + SERVICE_CLOSE_COVER, + intent as cover_intent, +) from homeassistant.helpers 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 + await cover_intent.async_setup_intents(hass) hass.states.async_set("cover.garage_door", "closed") calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) @@ -29,8 +31,7 @@ async def test_open_cover_intent(hass): async def test_close_cover_intent(hass): """Test HassCloseCover intent.""" - result = await comps.cover.async_setup(hass, {}) - assert result + await cover_intent.async_setup_intents(hass) hass.states.async_set("cover.garage_door", "open") calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER) diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py index e0e5f44873d665..76a0399c68876a 100644 --- a/tests/components/intent/test_init.py +++ b/tests/components/intent/test_init.py @@ -1,6 +1,11 @@ """Tests for Intent component.""" +import pytest + from homeassistant.setup import async_setup_component from homeassistant.helpers import intent +from homeassistant.components.cover import SERVICE_OPEN_COVER + +from tests.common import async_mock_service async def test_http_handle_intent(hass, hass_client, hass_admin_user): @@ -42,3 +47,30 @@ async def async_handle(self, intent): }, "speech": {"plain": {"extra_data": None, "speech": "I've ordered a Belgian!"}}, } + + +async def test_cover_intents_loading(hass): + """Test Cover Intents Loading.""" + assert await async_setup_component(hass, "intent", {}) + + with pytest.raises(intent.UnknownIntent): + await intent.async_handle( + hass, "test", "HassOpenCover", {"name": {"value": "garage door"}} + ) + + assert await async_setup_component(hass, "cover", {}) + + 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"} diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 8ceda6cbd3efa7..3539e109a66770 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -18,13 +18,10 @@ SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, - ATTR_SUPPORTED_FEATURES, ) from homeassistant.components import light -from homeassistant.helpers.intent import IntentHandleError from tests.common import ( - async_mock_service, mock_service, get_test_home_assistant, mock_storage, @@ -433,87 +430,6 @@ def _mock_open(path, *args, **kwargs): assert {light.ATTR_HS_COLOR: (50.353, 100), light.ATTR_BRIGHTNESS: 100} == data -async def test_intent_set_color(hass): - """Test the set color intent.""" - hass.states.async_set( - "light.hello_2", "off", {ATTR_SUPPORTED_FEATURES: light.SUPPORT_COLOR} - ) - hass.states.async_set("switch.hello", "off") - calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) - hass.helpers.intent.async_register(light.SetIntentHandler()) - - result = await hass.helpers.intent.async_handle( - "test", - light.INTENT_SET, - {"name": {"value": "Hello"}, "color": {"value": "blue"}}, - ) - await hass.async_block_till_done() - - assert result.speech["plain"]["speech"] == "Changed hello 2 to the color blue" - - assert len(calls) == 1 - call = calls[0] - assert call.domain == light.DOMAIN - assert call.service == SERVICE_TURN_ON - assert call.data.get(ATTR_ENTITY_ID) == "light.hello_2" - assert call.data.get(light.ATTR_RGB_COLOR) == (0, 0, 255) - - -async def test_intent_set_color_tests_feature(hass): - """Test the set color intent.""" - hass.states.async_set("light.hello", "off") - calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) - hass.helpers.intent.async_register(light.SetIntentHandler()) - - try: - await hass.helpers.intent.async_handle( - "test", - light.INTENT_SET, - {"name": {"value": "Hello"}, "color": {"value": "blue"}}, - ) - assert False, "handling intent should have raised" - except IntentHandleError as err: - assert str(err) == "Entity hello does not support changing colors" - - assert len(calls) == 0 - - -async def test_intent_set_color_and_brightness(hass): - """Test the set color intent.""" - hass.states.async_set( - "light.hello_2", - "off", - {ATTR_SUPPORTED_FEATURES: (light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS)}, - ) - hass.states.async_set("switch.hello", "off") - calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) - hass.helpers.intent.async_register(light.SetIntentHandler()) - - result = await hass.helpers.intent.async_handle( - "test", - light.INTENT_SET, - { - "name": {"value": "Hello"}, - "color": {"value": "blue"}, - "brightness": {"value": "20"}, - }, - ) - await hass.async_block_till_done() - - assert ( - result.speech["plain"]["speech"] - == "Changed hello 2 to the color blue and 20% brightness" - ) - - assert len(calls) == 1 - call = calls[0] - assert call.domain == light.DOMAIN - assert call.service == SERVICE_TURN_ON - assert call.data.get(ATTR_ENTITY_ID) == "light.hello_2" - assert call.data.get(light.ATTR_RGB_COLOR) == (0, 0, 255) - assert call.data.get(light.ATTR_BRIGHTNESS_PCT) == 20 - - async def test_light_context(hass, hass_admin_user): """Test that light context works.""" assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) diff --git a/tests/components/light/test_intent.py b/tests/components/light/test_intent.py new file mode 100644 index 00000000000000..594c9a5d1fc77e --- /dev/null +++ b/tests/components/light/test_intent.py @@ -0,0 +1,88 @@ +"""Tests for the light intents.""" +from homeassistant.helpers.intent import IntentHandleError + +from homeassistant.const import ATTR_SUPPORTED_FEATURES, SERVICE_TURN_ON, ATTR_ENTITY_ID +from homeassistant.components import light +from homeassistant.components.light import intent +from tests.common import async_mock_service + + +async def test_intent_set_color(hass): + """Test the set color intent.""" + hass.states.async_set( + "light.hello_2", "off", {ATTR_SUPPORTED_FEATURES: light.SUPPORT_COLOR} + ) + hass.states.async_set("switch.hello", "off") + calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) + await intent.async_setup_intents(hass) + + result = await hass.helpers.intent.async_handle( + "test", + intent.INTENT_SET, + {"name": {"value": "Hello"}, "color": {"value": "blue"}}, + ) + await hass.async_block_till_done() + + assert result.speech["plain"]["speech"] == "Changed hello 2 to the color blue" + + assert len(calls) == 1 + call = calls[0] + assert call.domain == light.DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data.get(ATTR_ENTITY_ID) == "light.hello_2" + assert call.data.get(light.ATTR_RGB_COLOR) == (0, 0, 255) + + +async def test_intent_set_color_tests_feature(hass): + """Test the set color intent.""" + hass.states.async_set("light.hello", "off") + calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) + await intent.async_setup_intents(hass) + + try: + await hass.helpers.intent.async_handle( + "test", + intent.INTENT_SET, + {"name": {"value": "Hello"}, "color": {"value": "blue"}}, + ) + assert False, "handling intent should have raised" + except IntentHandleError as err: + assert str(err) == "Entity hello does not support changing colors" + + assert len(calls) == 0 + + +async def test_intent_set_color_and_brightness(hass): + """Test the set color intent.""" + hass.states.async_set( + "light.hello_2", + "off", + {ATTR_SUPPORTED_FEATURES: (light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS)}, + ) + hass.states.async_set("switch.hello", "off") + calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) + await intent.async_setup_intents(hass) + + result = await hass.helpers.intent.async_handle( + "test", + intent.INTENT_SET, + { + "name": {"value": "Hello"}, + "color": {"value": "blue"}, + "brightness": {"value": "20"}, + }, + ) + await hass.async_block_till_done() + + assert ( + result.speech["plain"]["speech"] + == "Changed hello 2 to the color blue and 20% brightness" + ) + + assert len(calls) == 1 + call = calls[0] + assert call.domain == light.DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data.get(ATTR_ENTITY_ID) == "light.hello_2" + assert call.data.get(light.ATTR_RGB_COLOR) == (0, 0, 255) + assert call.data.get(light.ATTR_BRIGHTNESS_PCT) == 20 diff --git a/tests/components/shopping_list/conftest.py b/tests/components/shopping_list/conftest.py new file mode 100644 index 00000000000000..646b3bee4c01c9 --- /dev/null +++ b/tests/components/shopping_list/conftest.py @@ -0,0 +1,23 @@ +"""Shopping list test helpers.""" +from unittest.mock import patch + +import pytest + +from homeassistant.setup import async_setup_component +from homeassistant.components.shopping_list import intent as sl_intent + + +@pytest.fixture(autouse=True) +def mock_shopping_list_io(): + """Stub out the persistence.""" + with patch("homeassistant.components.shopping_list.ShoppingData.save"), patch( + "homeassistant.components.shopping_list." "ShoppingData.async_load" + ): + yield + + +@pytest.fixture +async def sl_setup(hass): + """Set up the shopping list.""" + assert await async_setup_component(hass, "shopping_list", {}) + await sl_intent.async_setup_intents(hass) diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py index 1d42fa60d9c26a..4394a835f494c3 100644 --- a/tests/components/shopping_list/test_init.py +++ b/tests/components/shopping_list/test_init.py @@ -1,27 +1,13 @@ """Test shopping list component.""" import asyncio -from unittest.mock import patch -import pytest - -from homeassistant.bootstrap import async_setup_component from homeassistant.helpers import intent from homeassistant.components.websocket_api.const import TYPE_RESULT -@pytest.fixture(autouse=True) -def mock_shopping_list_io(): - """Stub out the persistence.""" - with patch("homeassistant.components.shopping_list.ShoppingData.save"), patch( - "homeassistant.components.shopping_list." "ShoppingData.async_load" - ): - yield - - @asyncio.coroutine -def test_add_item(hass): +def test_add_item(hass, sl_setup): """Test adding an item intent.""" - yield from async_setup_component(hass, "shopping_list", {}) response = yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -31,9 +17,8 @@ def test_add_item(hass): @asyncio.coroutine -def test_recent_items_intent(hass): +def test_recent_items_intent(hass, sl_setup): """Test recent items.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -54,9 +39,8 @@ def test_recent_items_intent(hass): @asyncio.coroutine -def test_deprecated_api_get_all(hass, hass_client): +def test_deprecated_api_get_all(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -77,9 +61,8 @@ def test_deprecated_api_get_all(hass, hass_client): assert not data[1]["complete"] -async def test_ws_get_items(hass, hass_ws_client): +async def test_ws_get_items(hass, hass_ws_client, sl_setup): """Test get shopping_list items websocket command.""" - await async_setup_component(hass, "shopping_list", {}) await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -106,9 +89,8 @@ async def test_ws_get_items(hass, hass_ws_client): @asyncio.coroutine -def test_deprecated_api_update(hass, hass_client): +def test_deprecated_api_update(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -142,9 +124,8 @@ def test_deprecated_api_update(hass, hass_client): assert wine == {"id": wine_id, "name": "wine", "complete": True} -async def test_ws_update_item(hass, hass_ws_client): +async def test_ws_update_item(hass, hass_ws_client, sl_setup): """Test update shopping_list item websocket command.""" - await async_setup_component(hass, "shopping_list", {}) await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) @@ -186,9 +167,8 @@ async def test_ws_update_item(hass, hass_ws_client): @asyncio.coroutine -def test_api_update_fails(hass, hass_client): +def test_api_update_fails(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -209,9 +189,8 @@ def test_api_update_fails(hass, hass_client): assert resp.status == 400 -async def test_ws_update_item_fail(hass, hass_ws_client): +async def test_ws_update_item_fail(hass, hass_ws_client, sl_setup): """Test failure of update shopping_list item websocket command.""" - await async_setup_component(hass, "shopping_list", {}) await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) @@ -234,9 +213,8 @@ async def test_ws_update_item_fail(hass, hass_ws_client): @asyncio.coroutine -def test_deprecated_api_clear_completed(hass, hass_client): +def test_deprecated_api_clear_completed(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -265,9 +243,8 @@ def test_deprecated_api_clear_completed(hass, hass_client): assert items[0] == {"id": wine_id, "name": "wine", "complete": False} -async def test_ws_clear_items(hass, hass_ws_client): +async def test_ws_clear_items(hass, hass_ws_client, sl_setup): """Test clearing shopping_list items websocket command.""" - await async_setup_component(hass, "shopping_list", {}) await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) @@ -296,9 +273,8 @@ async def test_ws_clear_items(hass, hass_ws_client): @asyncio.coroutine -def test_deprecated_api_create(hass, hass_client): +def test_deprecated_api_create(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) client = yield from hass_client() resp = yield from client.post("/api/shopping_list/item", json={"name": "soda"}) @@ -315,9 +291,8 @@ def test_deprecated_api_create(hass, hass_client): @asyncio.coroutine -def test_deprecated_api_create_fail(hass, hass_client): +def test_deprecated_api_create_fail(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) client = yield from hass_client() resp = yield from client.post("/api/shopping_list/item", json={"name": 1234}) @@ -326,9 +301,8 @@ def test_deprecated_api_create_fail(hass, hass_client): assert len(hass.data["shopping_list"].items) == 0 -async def test_ws_add_item(hass, hass_ws_client): +async def test_ws_add_item(hass, hass_ws_client, sl_setup): """Test adding shopping_list item websocket command.""" - await async_setup_component(hass, "shopping_list", {}) client = await hass_ws_client(hass) await client.send_json({"id": 5, "type": "shopping_list/items/add", "name": "soda"}) msg = await client.receive_json() @@ -342,9 +316,8 @@ async def test_ws_add_item(hass, hass_ws_client): assert items[0]["complete"] is False -async def test_ws_add_item_fail(hass, hass_ws_client): +async def test_ws_add_item_fail(hass, hass_ws_client, sl_setup): """Test adding shopping_list item failure websocket command.""" - await async_setup_component(hass, "shopping_list", {}) client = await hass_ws_client(hass) await client.send_json({"id": 5, "type": "shopping_list/items/add", "name": 123}) msg = await client.receive_json() diff --git a/tests/components/shopping_list/test_intent.py b/tests/components/shopping_list/test_intent.py new file mode 100644 index 00000000000000..d0bcb1d837c2aa --- /dev/null +++ b/tests/components/shopping_list/test_intent.py @@ -0,0 +1,22 @@ +"""Test Shopping List intents.""" +from homeassistant.helpers import intent + + +async def test_recent_items_intent(hass, sl_setup): + """Test recent items.""" + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "wine"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "soda"}} + ) + + response = await intent.async_handle(hass, "test", "HassShoppingListLastItems") + + assert ( + response.speech["plain"]["speech"] + == "These are the top 3 items on your shopping list: soda, wine, beer" + ) From b8df63556886672883aad7f5e40555d7efe9da65 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Dec 2019 08:21:11 -0800 Subject: [PATCH 2/2] Lint --- homeassistant/components/light/intent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/intent.py b/homeassistant/components/light/intent.py index b8d5e82cd6981e..93b9748fc4a3da 100644 --- a/homeassistant/components/light/intent.py +++ b/homeassistant/components/light/intent.py @@ -1,7 +1,7 @@ """Intents for the light integration.""" -from homeassistant.core import HomeAssistant import voluptuous as vol +from homeassistant.core import HomeAssistant from homeassistant.helpers import intent import homeassistant.util.color as color_util import homeassistant.helpers.config_validation as cv