From 34194da1b6a16f946c06c3800851b3ef2da8e43b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Marowsky-Br=C3=A9e?= Date: Wed, 27 Jan 2021 11:17:59 +0100 Subject: [PATCH] New methods for input_select component (#42968) This adds a `cycle` attribute to select_previous/next, and select_first and select_last services. This is quite useful for streamlining using input_select via automations, such as when they represent a list of states to step through; if the first option is the dimmest and the last the brightest, one may not want to accidentally cycle from the first to the last, for example. Similarly, being able to directly select the first or last removes adjustment in related automations. --- .../components/input_select/__init__.py | 53 +++++++++++++--- .../components/input_select/services.yaml | 18 ++++++ tests/components/input_select/test_init.py | 60 +++++++++++++++++++ 3 files changed, 124 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 9269dc3a7f9..6272992f243 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -29,10 +29,13 @@ CONF_OPTIONS = "options" ATTR_OPTION = "option" ATTR_OPTIONS = "options" +ATTR_CYCLE = "cycle" SERVICE_SELECT_OPTION = "select_option" SERVICE_SELECT_NEXT = "select_next" SERVICE_SELECT_PREVIOUS = "select_previous" +SERVICE_SELECT_FIRST = "select_first" +SERVICE_SELECT_LAST = "select_last" SERVICE_SET_OPTIONS = "set_options" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 @@ -141,14 +144,26 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_SELECT_NEXT, - {}, - callback(lambda entity, call: entity.async_offset_index(1)), + {vol.Optional(ATTR_CYCLE, default=True): bool}, + "async_next", ) component.async_register_entity_service( SERVICE_SELECT_PREVIOUS, + {vol.Optional(ATTR_CYCLE, default=True): bool}, + "async_previous", + ) + + component.async_register_entity_service( + SERVICE_SELECT_FIRST, {}, - callback(lambda entity, call: entity.async_offset_index(-1)), + callback(lambda entity, call: entity.async_select_index(0)), + ) + + component.async_register_entity_service( + SERVICE_SELECT_LAST, + {}, + callback(lambda entity, call: entity.async_select_index(-1)), ) component.async_register_entity_service( @@ -263,13 +278,37 @@ class InputSelect(RestoreEntity): self.async_write_ha_state() @callback - def async_offset_index(self, offset): - """Offset current index.""" - current_index = self._options.index(self._current_option) - new_index = (current_index + offset) % len(self._options) + def async_select_index(self, idx): + """Select new option by index.""" + new_index = idx % len(self._options) self._current_option = self._options[new_index] self.async_write_ha_state() + @callback + def async_offset_index(self, offset, cycle): + """Offset current index.""" + current_index = self._options.index(self._current_option) + new_index = current_index + offset + if cycle: + new_index = new_index % len(self._options) + else: + if new_index < 0: + new_index = 0 + elif new_index >= len(self._options): + new_index = len(self._options) - 1 + self._current_option = self._options[new_index] + self.async_write_ha_state() + + @callback + def async_next(self, cycle): + """Select next option.""" + self.async_offset_index(1, cycle) + + @callback + def async_previous(self, cycle): + """Select previous option.""" + self.async_offset_index(-1, cycle) + @callback def async_set_options(self, options): """Set options.""" diff --git a/homeassistant/components/input_select/services.yaml b/homeassistant/components/input_select/services.yaml index f4aa44f3de0..0eddb158d34 100644 --- a/homeassistant/components/input_select/services.yaml +++ b/homeassistant/components/input_select/services.yaml @@ -4,6 +4,9 @@ select_next: entity_id: description: Entity id of the input select to select the next value for. example: input_select.my_select + cycle: + description: If the option should cycle from the last to the first (defaults to true). + example: true select_option: description: Select an option of an input select entity. fields: @@ -19,6 +22,21 @@ select_previous: entity_id: description: Entity id of the input select to select the previous value for. example: input_select.my_select + cycle: + description: If the option should cycle from the first to the last (defaults to true). + example: true +select_first: + description: Select the first option of an input select entity. + fields: + entity_id: + description: Entity id of the input select to select the first value for. + example: input_select.my_select +select_last: + description: Select the last option of an input select entity. + fields: + entity_id: + description: Entity id of the input select to select the last value for. + example: input_select.my_select set_options: description: Set the options of an input select entity. fields: diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index 5c470ca5bfc..38a3c3ba7a2 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -9,6 +9,8 @@ from homeassistant.components.input_select import ( ATTR_OPTIONS, CONF_INITIAL, DOMAIN, + SERVICE_SELECT_FIRST, + SERVICE_SELECT_LAST, SERVICE_SELECT_NEXT, SERVICE_SELECT_OPTION, SERVICE_SELECT_PREVIOUS, @@ -104,6 +106,32 @@ def select_previous(hass, entity_id): ) +@bind_hass +def select_first(hass, entity_id): + """Set first value of input_select. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.async_create_task( + hass.services.async_call( + DOMAIN, SERVICE_SELECT_FIRST, {ATTR_ENTITY_ID: entity_id} + ) + ) + + +@bind_hass +def select_last(hass, entity_id): + """Set last value of input_select. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.async_create_task( + hass.services.async_call( + DOMAIN, SERVICE_SELECT_LAST, {ATTR_ENTITY_ID: entity_id} + ) + ) + + async def test_config(hass): """Test config.""" invalid_configs = [ @@ -207,6 +235,38 @@ async def test_select_previous(hass): assert "last option" == state.state +async def test_select_first_last(hass): + """Test select_first and _last methods.""" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "test_1": { + "options": ["first option", "middle option", "last option"], + "initial": "middle option", + } + } + }, + ) + entity_id = "input_select.test_1" + + state = hass.states.get(entity_id) + assert "middle option" == state.state + + select_first(hass, entity_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert "first option" == state.state + + select_last(hass, entity_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert "last option" == state.state + + async def test_config_options(hass): """Test configuration options.""" count_start = len(hass.states.async_entity_ids())