From 7c9ef39ef6bdf1c6e14776fcf4c8d16ae4a8fc86 Mon Sep 17 00:00:00 2001 From: Shulyaka Date: Fri, 17 Jul 2020 21:20:34 +0300 Subject: [PATCH] Add humidifier intents (#37335) Co-authored-by: Paulus Schoutsen --- homeassistant/components/humidifier/intent.py | 127 +++++++++++ tests/components/humidifier/test_intent.py | 208 ++++++++++++++++++ 2 files changed, 335 insertions(+) create mode 100644 homeassistant/components/humidifier/intent.py create mode 100644 tests/components/humidifier/test_intent.py diff --git a/homeassistant/components/humidifier/intent.py b/homeassistant/components/humidifier/intent.py new file mode 100644 index 00000000000..ee257cc7123 --- /dev/null +++ b/homeassistant/components/humidifier/intent.py @@ -0,0 +1,127 @@ +"""Intents for the humidifier integration.""" +import voluptuous as vol + +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent +import homeassistant.helpers.config_validation as cv + +from . import ( + ATTR_AVAILABLE_MODES, + ATTR_HUMIDITY, + ATTR_MODE, + DOMAIN, + SERVICE_SET_HUMIDITY, + SERVICE_SET_MODE, + SERVICE_TURN_ON, + SUPPORT_MODES, +) + +INTENT_HUMIDITY = "HassHumidifierSetpoint" +INTENT_MODE = "HassHumidifierMode" + + +async def async_setup_intents(hass: HomeAssistant) -> None: + """Set up the humidifier intents.""" + hass.helpers.intent.async_register(HumidityHandler()) + hass.helpers.intent.async_register(SetModeHandler()) + + +class HumidityHandler(intent.IntentHandler): + """Handle set humidity intents.""" + + intent_type = INTENT_HUMIDITY + slot_schema = { + vol.Required("name"): cv.string, + vol.Required("humidity"): 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} + + humidity = slots["humidity"]["value"] + + if state.state == STATE_OFF: + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, service_data, context=intent_obj.context + ) + speech = f"Turned {state.name} on and set humidity to {humidity}%" + else: + speech = f"The {state.name} is set to {humidity}%" + + service_data[ATTR_HUMIDITY] = humidity + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HUMIDITY, + service_data, + context=intent_obj.context, + blocking=True, + ) + + response = intent_obj.create_response() + + response.async_set_speech(speech) + return response + + +class SetModeHandler(intent.IntentHandler): + """Handle set humidity intents.""" + + intent_type = INTENT_MODE + slot_schema = { + vol.Required("name"): cv.string, + vol.Required("mode"): cv.string, + } + + 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} + + intent.async_test_feature(state, SUPPORT_MODES, "modes") + mode = slots["mode"]["value"] + + if mode not in state.attributes.get(ATTR_AVAILABLE_MODES, []): + raise intent.IntentHandleError( + f"Entity {state.name} does not support {mode} mode" + ) + + if state.state == STATE_OFF: + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + service_data, + context=intent_obj.context, + blocking=True, + ) + speech = f"Turned {state.name} on and set {mode} mode" + else: + speech = f"The mode for {state.name} is set to {mode}" + + service_data[ATTR_MODE] = mode + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MODE, + service_data, + context=intent_obj.context, + blocking=True, + ) + + response = intent_obj.create_response() + + response.async_set_speech(speech) + return response diff --git a/tests/components/humidifier/test_intent.py b/tests/components/humidifier/test_intent.py new file mode 100644 index 00000000000..18c5b632aa6 --- /dev/null +++ b/tests/components/humidifier/test_intent.py @@ -0,0 +1,208 @@ +"""Tests for the humidifier intents.""" +from homeassistant.components.humidifier import ( + ATTR_AVAILABLE_MODES, + ATTR_HUMIDITY, + ATTR_MODE, + DOMAIN, + SERVICE_SET_HUMIDITY, + SERVICE_SET_MODE, + intent, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.helpers.intent import IntentHandleError + +from tests.common import async_mock_service + + +async def test_intent_set_humidity(hass): + """Test the set humidity intent.""" + hass.states.async_set( + "humidifier.bedroom_humidifier", STATE_ON, {ATTR_HUMIDITY: 40} + ) + humidity_calls = async_mock_service(hass, DOMAIN, SERVICE_SET_HUMIDITY) + turn_on_calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + await intent.async_setup_intents(hass) + + result = await hass.helpers.intent.async_handle( + "test", + intent.INTENT_HUMIDITY, + {"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}}, + ) + await hass.async_block_till_done() + + assert result.speech["plain"]["speech"] == "The bedroom humidifier is set to 50%" + + assert len(turn_on_calls) == 0 + assert len(humidity_calls) == 1 + call = humidity_calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_SET_HUMIDITY + assert call.data.get(ATTR_ENTITY_ID) == "humidifier.bedroom_humidifier" + assert call.data.get(ATTR_HUMIDITY) == 50 + + +async def test_intent_set_humidity_and_turn_on(hass): + """Test the set humidity intent for turned off humidifier.""" + hass.states.async_set( + "humidifier.bedroom_humidifier", STATE_OFF, {ATTR_HUMIDITY: 40} + ) + humidity_calls = async_mock_service(hass, DOMAIN, SERVICE_SET_HUMIDITY) + turn_on_calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + await intent.async_setup_intents(hass) + + result = await hass.helpers.intent.async_handle( + "test", + intent.INTENT_HUMIDITY, + {"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}}, + ) + await hass.async_block_till_done() + + assert ( + result.speech["plain"]["speech"] + == "Turned bedroom humidifier on and set humidity to 50%" + ) + + assert len(turn_on_calls) == 1 + call = turn_on_calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data.get(ATTR_ENTITY_ID) == "humidifier.bedroom_humidifier" + assert len(humidity_calls) == 1 + call = humidity_calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_SET_HUMIDITY + assert call.data.get(ATTR_ENTITY_ID) == "humidifier.bedroom_humidifier" + assert call.data.get(ATTR_HUMIDITY) == 50 + + +async def test_intent_set_mode(hass): + """Test the set mode intent.""" + hass.states.async_set( + "humidifier.bedroom_humidifier", + STATE_ON, + { + ATTR_HUMIDITY: 40, + ATTR_SUPPORTED_FEATURES: 1, + ATTR_AVAILABLE_MODES: ["home", "away"], + ATTR_MODE: "home", + }, + ) + mode_calls = async_mock_service(hass, DOMAIN, SERVICE_SET_MODE) + turn_on_calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + await intent.async_setup_intents(hass) + + result = await hass.helpers.intent.async_handle( + "test", + intent.INTENT_MODE, + {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, + ) + await hass.async_block_till_done() + + assert ( + result.speech["plain"]["speech"] + == "The mode for bedroom humidifier is set to away" + ) + + assert len(turn_on_calls) == 0 + assert len(mode_calls) == 1 + call = mode_calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_SET_MODE + assert call.data.get(ATTR_ENTITY_ID) == "humidifier.bedroom_humidifier" + assert call.data.get(ATTR_MODE) == "away" + + +async def test_intent_set_mode_and_turn_on(hass): + """Test the set mode intent.""" + hass.states.async_set( + "humidifier.bedroom_humidifier", + STATE_OFF, + { + ATTR_HUMIDITY: 40, + ATTR_SUPPORTED_FEATURES: 1, + ATTR_AVAILABLE_MODES: ["home", "away"], + ATTR_MODE: "home", + }, + ) + mode_calls = async_mock_service(hass, DOMAIN, SERVICE_SET_MODE) + turn_on_calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + await intent.async_setup_intents(hass) + + result = await hass.helpers.intent.async_handle( + "test", + intent.INTENT_MODE, + {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, + ) + await hass.async_block_till_done() + + assert ( + result.speech["plain"]["speech"] + == "Turned bedroom humidifier on and set away mode" + ) + + assert len(turn_on_calls) == 1 + call = turn_on_calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data.get(ATTR_ENTITY_ID) == "humidifier.bedroom_humidifier" + assert len(mode_calls) == 1 + call = mode_calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_SET_MODE + assert call.data.get(ATTR_ENTITY_ID) == "humidifier.bedroom_humidifier" + assert call.data.get(ATTR_MODE) == "away" + + +async def test_intent_set_mode_tests_feature(hass): + """Test the set mode intent where modes are not supported.""" + hass.states.async_set( + "humidifier.bedroom_humidifier", STATE_ON, {ATTR_HUMIDITY: 40} + ) + mode_calls = async_mock_service(hass, DOMAIN, SERVICE_SET_MODE) + await intent.async_setup_intents(hass) + + try: + await hass.helpers.intent.async_handle( + "test", + intent.INTENT_MODE, + {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, + ) + assert False, "handling intent should have raised" + except IntentHandleError as err: + assert str(err) == "Entity bedroom humidifier does not support modes" + + assert len(mode_calls) == 0 + + +async def test_intent_set_unknown_mode(hass): + """Test the set mode intent for unsupported mode.""" + hass.states.async_set( + "humidifier.bedroom_humidifier", + STATE_ON, + { + ATTR_HUMIDITY: 40, + ATTR_SUPPORTED_FEATURES: 1, + ATTR_AVAILABLE_MODES: ["home", "away"], + ATTR_MODE: "home", + }, + ) + mode_calls = async_mock_service(hass, DOMAIN, SERVICE_SET_MODE) + await intent.async_setup_intents(hass) + + try: + await hass.helpers.intent.async_handle( + "test", + intent.INTENT_MODE, + {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "eco"}}, + ) + assert False, "handling intent should have raised" + except IntentHandleError as err: + assert str(err) == "Entity bedroom humidifier does not support eco mode" + + assert len(mode_calls) == 0