From 468b0e893470b0de5f881ece592b6cb2b1139e17 Mon Sep 17 00:00:00 2001 From: Luuk Date: Fri, 28 Jun 2019 18:19:00 +0200 Subject: [PATCH] Add template vacuum support (#22904) * Add template vacuum component * Fix linting issues * Make vacuum state optional * Fix pylint issues * Add context to template vacuum service calls * Added tests to template vacuum * Fix indent * Fix docstrings * Move files for new component folder structure * Revert additions for template_vacuum tests to common.py * Use existing constants for template vacuum config * Handle invalid templates * Add tests for unused services * Add test for invalid templates * Fix line too long * Do not start template change tracking in case of MATCH_ALL * Resolve review comments --- homeassistant/components/template/vacuum.py | 361 +++++++++++++++ tests/components/template/test_vacuum.py | 475 ++++++++++++++++++++ 2 files changed, 836 insertions(+) create mode 100644 homeassistant/components/template/vacuum.py create mode 100644 tests/components/template/test_vacuum.py diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py new file mode 100644 index 00000000000..d9f3b6bd6a7 --- /dev/null +++ b/homeassistant/components/template/vacuum.py @@ -0,0 +1,361 @@ +"""Support for Template vacuums.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.vacuum import ( + ATTR_FAN_SPEED, DOMAIN, SERVICE_CLEAN_SPOT, SERVICE_LOCATE, SERVICE_PAUSE, + SERVICE_RETURN_TO_BASE, SERVICE_SET_FAN_SPEED, SERVICE_START, + SERVICE_STOP, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, + SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_STOP, + SUPPORT_STATE, SUPPORT_START, StateVacuumDevice, STATE_CLEANING, + STATE_DOCKED, STATE_PAUSED, STATE_IDLE, STATE_RETURNING, STATE_ERROR) +from homeassistant.const import ( + CONF_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, CONF_ENTITY_ID, + MATCH_ALL, EVENT_HOMEASSISTANT_START, STATE_UNKNOWN) +from homeassistant.core import callback +from homeassistant.exceptions import TemplateError +from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.script import Script + +_LOGGER = logging.getLogger(__name__) + +CONF_VACUUMS = 'vacuums' +CONF_BATTERY_LEVEL_TEMPLATE = 'battery_level_template' +CONF_FAN_SPEED_LIST = 'fan_speeds' +CONF_FAN_SPEED_TEMPLATE = 'fan_speed_template' + +ENTITY_ID_FORMAT = DOMAIN + '.{}' +_VALID_STATES = [STATE_CLEANING, STATE_DOCKED, STATE_PAUSED, STATE_IDLE, + STATE_RETURNING, STATE_ERROR] + +VACUUM_SCHEMA = vol.Schema({ + vol.Optional(CONF_FRIENDLY_NAME): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_BATTERY_LEVEL_TEMPLATE): cv.template, + vol.Optional(CONF_FAN_SPEED_TEMPLATE): cv.template, + + vol.Required(SERVICE_START): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_PAUSE): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_STOP): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_RETURN_TO_BASE): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_CLEAN_SPOT): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_LOCATE): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_SET_FAN_SPEED): cv.SCRIPT_SCHEMA, + + vol.Optional( + CONF_FAN_SPEED_LIST, + default=[] + ): cv.ensure_list, + + vol.Optional(CONF_ENTITY_ID): cv.entity_ids +}) + +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ + vol.Required(CONF_VACUUMS): vol.Schema({cv.slug: VACUUM_SCHEMA}), +}) + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +): + """Set up the Template Vacuums.""" + vacuums = [] + + for device, device_config in config[CONF_VACUUMS].items(): + friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) + + state_template = device_config.get(CONF_VALUE_TEMPLATE) + battery_level_template = device_config.get(CONF_BATTERY_LEVEL_TEMPLATE) + fan_speed_template = device_config.get(CONF_FAN_SPEED_TEMPLATE) + + start_action = device_config[SERVICE_START] + pause_action = device_config.get(SERVICE_PAUSE) + stop_action = device_config.get(SERVICE_STOP) + return_to_base_action = device_config.get(SERVICE_RETURN_TO_BASE) + clean_spot_action = device_config.get(SERVICE_CLEAN_SPOT) + locate_action = device_config.get(SERVICE_LOCATE) + set_fan_speed_action = device_config.get(SERVICE_SET_FAN_SPEED) + + fan_speed_list = device_config[CONF_FAN_SPEED_LIST] + + entity_ids = set() + manual_entity_ids = device_config.get(CONF_ENTITY_ID) + invalid_templates = [] + + for tpl_name, template in ( + (CONF_VALUE_TEMPLATE, state_template), + (CONF_BATTERY_LEVEL_TEMPLATE, battery_level_template), + (CONF_FAN_SPEED_TEMPLATE, fan_speed_template) + ): + if template is None: + continue + template.hass = hass + + if manual_entity_ids is not None: + continue + + template_entity_ids = template.extract_entities() + if template_entity_ids == MATCH_ALL: + entity_ids = MATCH_ALL + # Cut off _template from name + invalid_templates.append(tpl_name[:-9]) + elif entity_ids != MATCH_ALL: + entity_ids |= set(template_entity_ids) + + if invalid_templates: + _LOGGER.warning( + 'Template vacuum %s has no entity ids configured to track nor' + ' were we able to extract the entities to track from the %s ' + 'template(s). This entity will only be able to be updated ' + 'manually.', device, ', '.join(invalid_templates)) + + if manual_entity_ids is not None: + entity_ids = manual_entity_ids + elif entity_ids != MATCH_ALL: + entity_ids = list(entity_ids) + + vacuums.append( + TemplateVacuum( + hass, device, friendly_name, + state_template, battery_level_template, fan_speed_template, + start_action, pause_action, stop_action, return_to_base_action, + clean_spot_action, locate_action, set_fan_speed_action, + fan_speed_list, entity_ids + ) + ) + + async_add_entities(vacuums) + + +class TemplateVacuum(StateVacuumDevice): + """A template vacuum component.""" + + def __init__(self, hass, device_id, friendly_name, + state_template, battery_level_template, fan_speed_template, + start_action, pause_action, stop_action, + return_to_base_action, clean_spot_action, locate_action, + set_fan_speed_action, fan_speed_list, entity_ids): + """Initialize the vacuum.""" + self.hass = hass + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, device_id, hass=hass) + self._name = friendly_name + + self._template = state_template + self._battery_level_template = battery_level_template + self._fan_speed_template = fan_speed_template + self._supported_features = SUPPORT_START + + self._start_script = Script(hass, start_action) + + self._pause_script = None + if pause_action: + self._pause_script = Script(hass, pause_action) + self._supported_features |= SUPPORT_PAUSE + + self._stop_script = None + if stop_action: + self._stop_script = Script(hass, stop_action) + self._supported_features |= SUPPORT_STOP + + self._return_to_base_script = None + if return_to_base_action: + self._return_to_base_script = Script(hass, return_to_base_action) + self._supported_features |= SUPPORT_RETURN_HOME + + self._clean_spot_script = None + if clean_spot_action: + self._clean_spot_script = Script(hass, clean_spot_action) + self._supported_features |= SUPPORT_CLEAN_SPOT + + self._locate_script = None + if locate_action: + self._locate_script = Script(hass, locate_action) + self._supported_features |= SUPPORT_LOCATE + + self._set_fan_speed_script = None + if set_fan_speed_action: + self._set_fan_speed_script = Script(hass, set_fan_speed_action) + self._supported_features |= SUPPORT_FAN_SPEED + + self._state = None + self._battery_level = None + self._fan_speed = None + + if self._template: + self._supported_features |= SUPPORT_STATE + if self._battery_level_template: + self._supported_features |= SUPPORT_BATTERY + + self._entities = entity_ids + # List of valid fan speeds + self._fan_speed_list = fan_speed_list + + @property + def name(self): + """Return the display name of this vacuum.""" + return self._name + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return self._supported_features + + @property + def state(self): + """Return the status of the vacuum cleaner.""" + return self._state + + @property + def battery_level(self): + """Return the battery level of the vacuum cleaner.""" + return self._battery_level + + @property + def fan_speed(self): + """Return the fan speed of the vacuum cleaner.""" + return self._fan_speed + + @property + def fan_speed_list(self) -> list: + """Get the list of available fan speeds.""" + return self._fan_speed_list + + @property + def should_poll(self): + """Return the polling state.""" + return False + + async def async_start(self): + """Start or resume the cleaning task.""" + await self._start_script.async_run(context=self._context) + + async def async_pause(self): + """Pause the cleaning task.""" + if self._pause_script is None: + return + + await self._pause_script.async_run(context=self._context) + + async def async_stop(self, **kwargs): + """Stop the cleaning task.""" + if self._stop_script is None: + return + + await self._stop_script.async_run(context=self._context) + + async def async_return_to_base(self, **kwargs): + """Set the vacuum cleaner to return to the dock.""" + if self._return_to_base_script is None: + return + + await self._return_to_base_script.async_run(context=self._context) + + async def async_clean_spot(self, **kwargs): + """Perform a spot clean-up.""" + if self._clean_spot_script is None: + return + + await self._clean_spot_script.async_run(context=self._context) + + async def async_locate(self, **kwargs): + """Locate the vacuum cleaner.""" + if self._locate_script is None: + return + + await self._locate_script.async_run(context=self._context) + + async def async_set_fan_speed(self, fan_speed, **kwargs): + """Set fan speed.""" + if self._set_fan_speed_script is None: + return + + if fan_speed in self._fan_speed_list: + self._fan_speed = fan_speed + await self._set_fan_speed_script.async_run( + {ATTR_FAN_SPEED: fan_speed}, context=self._context) + else: + _LOGGER.error( + 'Received invalid fan speed: %s. Expected: %s.', + fan_speed, self._fan_speed_list) + + async def async_added_to_hass(self): + """Register callbacks.""" + @callback + def template_vacuum_state_listener(entity, old_state, new_state): + """Handle target device state changes.""" + self.async_schedule_update_ha_state(True) + + @callback + def template_vacuum_startup(event): + """Update template on startup.""" + if self._entities != MATCH_ALL: + # Track state changes only for valid templates + self.hass.helpers.event.async_track_state_change( + self._entities, template_vacuum_state_listener) + + self.async_schedule_update_ha_state(True) + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, template_vacuum_startup) + + async def async_update(self): + """Update the state from the template.""" + # Update state + if self._template is not None: + try: + state = self._template.async_render() + except TemplateError as ex: + _LOGGER.error(ex) + state = None + self._state = None + + # Validate state + if state in _VALID_STATES: + self._state = state + elif state == STATE_UNKNOWN: + self._state = None + else: + _LOGGER.error( + 'Received invalid vacuum state: %s. Expected: %s.', + state, ', '.join(_VALID_STATES)) + self._state = None + + # Update battery level if 'battery_level_template' is configured + if self._battery_level_template is not None: + try: + battery_level = self._battery_level_template.async_render() + except TemplateError as ex: + _LOGGER.error(ex) + battery_level = None + + # Validate battery level + if battery_level and 0 <= int(battery_level) <= 100: + self._battery_level = int(battery_level) + else: + _LOGGER.error( + 'Received invalid battery level: %s. Expected: 0-100', + battery_level) + self._battery_level = None + + # Update fan speed if 'fan_speed_template' is configured + if self._fan_speed_template is not None: + try: + fan_speed = self._fan_speed_template.async_render() + except TemplateError as ex: + _LOGGER.error(ex) + fan_speed = None + self._state = None + + # Validate fan speed + if fan_speed in self._fan_speed_list: + self._fan_speed = fan_speed + elif fan_speed == STATE_UNKNOWN: + self._fan_speed = None + else: + _LOGGER.error( + 'Received invalid fan speed: %s. Expected: %s.', + fan_speed, self._fan_speed_list) + self._fan_speed = None diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py new file mode 100644 index 00000000000..ab071b93316 --- /dev/null +++ b/tests/components/template/test_vacuum.py @@ -0,0 +1,475 @@ +"""The tests for the Template vacuum platform.""" +import logging +import pytest + +from homeassistant import setup +from homeassistant.const import (STATE_ON, STATE_UNKNOWN) +from homeassistant.components.vacuum import ( + ATTR_BATTERY_LEVEL, STATE_CLEANING, STATE_DOCKED, STATE_IDLE, + STATE_PAUSED, STATE_RETURNING) + +from tests.common import ( + async_mock_service, assert_setup_component) +from tests.components.vacuum import common + +_LOGGER = logging.getLogger(__name__) + +_TEST_VACUUM = 'vacuum.test_vacuum' +_STATE_INPUT_SELECT = 'input_select.state' +_SPOT_CLEANING_INPUT_BOOLEAN = 'input_boolean.spot_cleaning' +_LOCATING_INPUT_BOOLEAN = 'input_boolean.locating' +_FAN_SPEED_INPUT_SELECT = 'input_select.fan_speed' +_BATTERY_LEVEL_INPUT_NUMBER = 'input_number.battery_level' + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, 'test', 'automation') + + +# Configuration tests # +async def test_missing_optional_config(hass, calls): + """Test: missing optional template is ok.""" + with assert_setup_component(1, 'vacuum'): + assert await setup.async_setup_component(hass, 'vacuum', { + 'vacuum': { + 'platform': 'template', + 'vacuums': { + 'test_vacuum': { + 'start': { + 'service': 'script.vacuum_start' + } + } + } + } + }) + + await hass.async_start() + await hass.async_block_till_done() + + _verify(hass, STATE_UNKNOWN, None) + + +async def test_missing_start_config(hass, calls): + """Test: missing 'start' will fail.""" + with assert_setup_component(0, 'vacuum'): + assert await setup.async_setup_component(hass, 'vacuum', { + 'vacuum': { + 'platform': 'template', + 'vacuums': { + 'test_vacuum': { + 'value_template': "{{ 'on' }}" + } + } + } + }) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.async_all() == [] + + +async def test_invalid_config(hass, calls): + """Test: invalid config structure will fail.""" + with assert_setup_component(0, 'vacuum'): + assert await setup.async_setup_component(hass, 'vacuum', { + 'platform': 'template', + 'vacuums': { + 'test_vacuum': { + 'start': { + 'service': 'script.vacuum_start' + } + } + } + }) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.async_all() == [] + +# End of configuration tests # + + +# Template tests # +async def test_templates_with_entities(hass, calls): + """Test templates with values from other entities.""" + with assert_setup_component(1, 'vacuum'): + assert await setup.async_setup_component(hass, 'vacuum', { + 'vacuum': { + 'platform': 'template', + 'vacuums': { + 'test_vacuum': { + 'value_template': "{{ states('input_select.state') }}", + 'battery_level_template': + "{{ states('input_number.battery_level') }}", + 'start': { + 'service': 'script.vacuum_start' + } + } + } + } + }) + + await hass.async_start() + await hass.async_block_till_done() + + _verify(hass, STATE_UNKNOWN, None) + + hass.states.async_set(_STATE_INPUT_SELECT, STATE_CLEANING) + hass.states.async_set(_BATTERY_LEVEL_INPUT_NUMBER, 100) + await hass.async_block_till_done() + + _verify(hass, STATE_CLEANING, 100) + + +async def test_templates_with_valid_values(hass, calls): + """Test templates with valid values.""" + with assert_setup_component(1, 'vacuum'): + assert await setup.async_setup_component(hass, 'vacuum', { + 'vacuum': { + 'platform': 'template', + 'vacuums': { + 'test_vacuum': { + 'value_template': "{{ 'cleaning' }}", + 'battery_level_template': + "{{ 100 }}", + 'start': { + 'service': 'script.vacuum_start' + } + } + } + } + }) + + await hass.async_start() + await hass.async_block_till_done() + + _verify(hass, STATE_CLEANING, 100) + + +async def test_templates_invalid_values(hass, calls): + """Test templates with invalid values.""" + with assert_setup_component(1, 'vacuum'): + assert await setup.async_setup_component(hass, 'vacuum', { + 'vacuum': { + 'platform': 'template', + 'vacuums': { + 'test_vacuum': { + 'value_template': "{{ 'abc' }}", + 'battery_level_template': + "{{ 101 }}", + 'start': { + 'service': 'script.vacuum_start' + } + } + } + } + }) + + await hass.async_start() + await hass.async_block_till_done() + + _verify(hass, STATE_UNKNOWN, None) + + +async def test_invalid_templates(hass, calls): + """Test invalid templates.""" + with assert_setup_component(1, 'vacuum'): + assert await setup.async_setup_component(hass, 'vacuum', { + 'vacuum': { + 'platform': 'template', + 'vacuums': { + 'test_vacuum': { + 'value_template': + "{{ this_function_does_not_exist() }}", + 'battery_level_template': + "{{ this_function_does_not_exist() }}", + 'fan_speed_template': + "{{ this_function_does_not_exist() }}", + 'start': { + 'service': 'script.vacuum_start' + } + } + } + } + }) + + await hass.async_start() + await hass.async_block_till_done() + + _verify(hass, STATE_UNKNOWN, None) + +# End of template tests # + + +# Function tests # +async def test_state_services(hass, calls): + """Test state services.""" + await _register_components(hass) + + # Start vacuum + common.async_start(hass, _TEST_VACUUM) + await hass.async_block_till_done() + + # verify + assert hass.states.get(_STATE_INPUT_SELECT).state == STATE_CLEANING + _verify(hass, STATE_CLEANING, None) + + # Pause vacuum + common.async_pause(hass, _TEST_VACUUM) + await hass.async_block_till_done() + + # verify + assert hass.states.get(_STATE_INPUT_SELECT).state == STATE_PAUSED + _verify(hass, STATE_PAUSED, None) + + # Stop vacuum + common.async_stop(hass, _TEST_VACUUM) + await hass.async_block_till_done() + + # verify + assert hass.states.get(_STATE_INPUT_SELECT).state == STATE_IDLE + _verify(hass, STATE_IDLE, None) + + # Return vacuum to base + common.async_return_to_base(hass, _TEST_VACUUM) + await hass.async_block_till_done() + + # verify + assert hass.states.get(_STATE_INPUT_SELECT).state == STATE_RETURNING + _verify(hass, STATE_RETURNING, None) + + +async def test_unused_services(hass, calls): + """Test calling unused services should not crash.""" + await _register_basic_vacuum(hass) + + # Pause vacuum + common.async_pause(hass, _TEST_VACUUM) + await hass.async_block_till_done() + + # Stop vacuum + common.async_stop(hass, _TEST_VACUUM) + await hass.async_block_till_done() + + # Return vacuum to base + common.async_return_to_base(hass, _TEST_VACUUM) + await hass.async_block_till_done() + + # Spot cleaning + common.async_clean_spot(hass, _TEST_VACUUM) + await hass.async_block_till_done() + + # Locate vacuum + common.async_locate(hass, _TEST_VACUUM) + await hass.async_block_till_done() + + # Set fan's speed + common.async_set_fan_speed(hass, 'medium', _TEST_VACUUM) + await hass.async_block_till_done() + + _verify(hass, STATE_UNKNOWN, None) + + +async def test_clean_spot_service(hass, calls): + """Test clean spot service.""" + await _register_components(hass) + + # Clean spot + common.async_clean_spot(hass, _TEST_VACUUM) + await hass.async_block_till_done() + + # verify + assert hass.states.get(_SPOT_CLEANING_INPUT_BOOLEAN).state == STATE_ON + + +async def test_locate_service(hass, calls): + """Test locate service.""" + await _register_components(hass) + + # Locate vacuum + common.async_locate(hass, _TEST_VACUUM) + await hass.async_block_till_done() + + # verify + assert hass.states.get(_LOCATING_INPUT_BOOLEAN).state == STATE_ON + + +async def test_set_fan_speed(hass, calls): + """Test set valid fan speed.""" + await _register_components(hass) + + # Set vacuum's fan speed to high + common.async_set_fan_speed(hass, 'high', _TEST_VACUUM) + await hass.async_block_till_done() + + # verify + assert hass.states.get(_FAN_SPEED_INPUT_SELECT).state == 'high' + + # Set fan's speed to medium + common.async_set_fan_speed(hass, 'medium', _TEST_VACUUM) + await hass.async_block_till_done() + + # verify + assert hass.states.get(_FAN_SPEED_INPUT_SELECT).state == 'medium' + + +async def test_set_invalid_fan_speed(hass, calls): + """Test set invalid fan speed when fan has valid speed.""" + await _register_components(hass) + + # Set vacuum's fan speed to high + common.async_set_fan_speed(hass, 'high', _TEST_VACUUM) + await hass.async_block_till_done() + + # verify + assert hass.states.get(_FAN_SPEED_INPUT_SELECT).state == 'high' + + # Set vacuum's fan speed to 'invalid' + common.async_set_fan_speed(hass, 'invalid', _TEST_VACUUM) + await hass.async_block_till_done() + + # verify fan speed is unchanged + assert hass.states.get(_FAN_SPEED_INPUT_SELECT).state == 'high' + + +def _verify(hass, expected_state, expected_battery_level): + """Verify vacuum's state and speed.""" + state = hass.states.get(_TEST_VACUUM) + attributes = state.attributes + assert state.state == expected_state + assert attributes.get(ATTR_BATTERY_LEVEL) == expected_battery_level + + +async def _register_basic_vacuum(hass): + """Register basic vacuum with only required options for testing.""" + with assert_setup_component(1, 'input_select'): + assert await setup.async_setup_component(hass, 'input_select', { + 'input_select': { + 'state': { + 'name': 'State', + 'options': [STATE_CLEANING] + } + } + }) + + with assert_setup_component(1, 'vacuum'): + assert await setup.async_setup_component(hass, 'vacuum', { + 'vacuum': { + 'platform': 'template', + 'vacuums': { + 'test_vacuum': { + 'start': { + 'service': 'input_select.select_option', + + 'data': { + 'entity_id': _STATE_INPUT_SELECT, + 'option': STATE_CLEANING + } + } + } + } + } + }) + + await hass.async_start() + await hass.async_block_till_done() + + +async def _register_components(hass): + """Register basic components for testing.""" + with assert_setup_component(2, 'input_boolean'): + assert await setup.async_setup_component(hass, 'input_boolean', { + 'input_boolean': { + 'spot_cleaning': None, + 'locating': None + } + }) + + with assert_setup_component(2, 'input_select'): + assert await setup.async_setup_component(hass, 'input_select', { + 'input_select': { + 'state': { + 'name': 'State', + 'options': [STATE_CLEANING, STATE_DOCKED, STATE_IDLE, + STATE_PAUSED, STATE_RETURNING] + }, + + 'fan_speed': { + 'name': 'Fan speed', + 'options': ['', 'low', 'medium', 'high'] + } + } + }) + + with assert_setup_component(1, 'vacuum'): + test_vacuum_config = { + 'value_template': "{{ states('input_select.state') }}", + 'fan_speed_template': + "{{ states('input_select.fan_speed') }}", + + 'start': { + 'service': 'input_select.select_option', + + 'data': { + 'entity_id': _STATE_INPUT_SELECT, + 'option': STATE_CLEANING + } + }, + 'pause': { + 'service': 'input_select.select_option', + + 'data': { + 'entity_id': _STATE_INPUT_SELECT, + 'option': STATE_PAUSED + } + }, + 'stop': { + 'service': 'input_select.select_option', + + 'data': { + 'entity_id': _STATE_INPUT_SELECT, + 'option': STATE_IDLE + } + }, + 'return_to_base': { + 'service': 'input_select.select_option', + + 'data': { + 'entity_id': _STATE_INPUT_SELECT, + 'option': STATE_RETURNING + } + }, + 'clean_spot': { + 'service': 'input_boolean.turn_on', + 'entity_id': _SPOT_CLEANING_INPUT_BOOLEAN + }, + 'locate': { + 'service': 'input_boolean.turn_on', + 'entity_id': _LOCATING_INPUT_BOOLEAN + }, + 'set_fan_speed': { + 'service': 'input_select.select_option', + + 'data_template': { + 'entity_id': _FAN_SPEED_INPUT_SELECT, + 'option': '{{ fan_speed }}' + } + }, + 'fan_speeds': ['low', 'medium', 'high'] + } + + assert await setup.async_setup_component(hass, 'vacuum', { + 'vacuum': { + 'platform': 'template', + 'vacuums': { + 'test_vacuum': test_vacuum_config + } + } + }) + + await hass.async_start() + await hass.async_block_till_done()