From 1409b89af38ef76ef553851d83d38512d0bd2ad1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 3 Feb 2023 15:43:17 +0100 Subject: [PATCH] Sync input_select & select (#87255) --- .../components/input_select/__init__.py | 107 ++++--------- homeassistant/components/select/__init__.py | 109 +++++++++++++- homeassistant/components/select/const.py | 6 + .../components/select/device_action.py | 100 ++++++++++--- homeassistant/components/select/services.yaml | 42 ++++++ homeassistant/components/select/strings.json | 6 +- tests/components/select/test_device_action.py | 140 +++++++++++++++++- tests/components/select/test_init.py | 97 +++++++++++- tests/components/zha/test_device_action.py | 52 +++---- 9 files changed, 522 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index bd9c43e8538..1891ba42319 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -6,10 +6,19 @@ from typing import Any, cast import voluptuous as vol -from homeassistant.components.select import SelectEntity +from homeassistant.components.select import ( + ATTR_CYCLE, + ATTR_OPTION, + ATTR_OPTIONS, + SERVICE_SELECT_FIRST, + SERVICE_SELECT_LAST, + SERVICE_SELECT_NEXT, + SERVICE_SELECT_OPTION, + SERVICE_SELECT_PREVIOUS, + SelectEntity, +) from homeassistant.const import ( ATTR_EDITABLE, - ATTR_OPTION, CONF_ICON, CONF_ID, CONF_NAME, @@ -35,14 +44,6 @@ DOMAIN = "input_select" CONF_INITIAL = "initial" CONF_OPTIONS = "options" -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 @@ -187,34 +188,34 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: schema=RELOAD_SERVICE_SCHEMA, ) - component.async_register_entity_service( - SERVICE_SELECT_OPTION, - {vol.Required(ATTR_OPTION): cv.string}, - "async_select_option", - ) - - component.async_register_entity_service( - SERVICE_SELECT_NEXT, - {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_select_index(0)), + InputSelect.async_first.__name__, ) component.async_register_entity_service( SERVICE_SELECT_LAST, {}, - callback(lambda entity, call: entity.async_select_index(-1)), + InputSelect.async_last.__name__, + ) + + component.async_register_entity_service( + SERVICE_SELECT_NEXT, + {vol.Optional(ATTR_CYCLE, default=True): bool}, + InputSelect.async_next.__name__, + ) + + component.async_register_entity_service( + SERVICE_SELECT_OPTION, + {vol.Required(ATTR_OPTION): cv.string}, + InputSelect.async_select_option.__name__, + ) + + component.async_register_entity_service( + SERVICE_SELECT_PREVIOUS, + {vol.Optional(ATTR_CYCLE, default=True): bool}, + InputSelect.async_previous.__name__, ) component.async_register_entity_service( @@ -310,52 +311,6 @@ class InputSelect(collection.CollectionEntity, SelectEntity, RestoreEntity): self._attr_current_option = option self.async_write_ha_state() - @callback - def async_select_index(self, idx: int) -> None: - """Select new option by index.""" - new_index = idx % len(self.options) - self._attr_current_option = self.options[new_index] - self.async_write_ha_state() - - @callback - def async_offset_index(self, offset: int, cycle: bool) -> None: - """Offset current index.""" - - current_index = ( - self.options.index(self.current_option) - if self.current_option is not None - else 0 - ) - - new_index = current_index + offset - if cycle: - new_index = new_index % len(self.options) - elif new_index < 0: - new_index = 0 - elif new_index >= len(self.options): - new_index = len(self.options) - 1 - - self._attr_current_option = self.options[new_index] - self.async_write_ha_state() - - @callback - def async_next(self, cycle: bool) -> None: - """Select next option.""" - # If there is no current option, first item is the next - if self.current_option is None: - self.async_select_index(0) - return - self.async_offset_index(1, cycle) - - @callback - def async_previous(self, cycle: bool) -> None: - """Select previous option.""" - # If there is no current option, last item is the previous - if self.current_option is None: - self.async_select_index(-1) - return - self.async_offset_index(-1, cycle) - async def async_set_options(self, options: list[str]) -> None: """Set options.""" unique_options = list(dict.fromkeys(options)) diff --git a/homeassistant/components/select/__init__.py b/homeassistant/components/select/__init__.py index 20cbb86e3ae..e3c5c9cfe69 100644 --- a/homeassistant/components/select/__init__.py +++ b/homeassistant/components/select/__init__.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa: F401 +from homeassistant.helpers.config_validation import ( PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) @@ -19,7 +19,17 @@ from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType -from .const import ATTR_OPTION, ATTR_OPTIONS, DOMAIN, SERVICE_SELECT_OPTION +from .const import ( + ATTR_CYCLE, + ATTR_OPTION, + ATTR_OPTIONS, + DOMAIN, + SERVICE_SELECT_FIRST, + SERVICE_SELECT_LAST, + SERVICE_SELECT_NEXT, + SERVICE_SELECT_OPTION, + SERVICE_SELECT_PREVIOUS, +) SCAN_INTERVAL = timedelta(seconds=30) @@ -29,6 +39,22 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) _LOGGER = logging.getLogger(__name__) +__all__ = [ + "ATTR_CYCLE", + "ATTR_OPTION", + "ATTR_OPTIONS", + "DOMAIN", + "PLATFORM_SCHEMA_BASE", + "PLATFORM_SCHEMA", + "SelectEntity", + "SelectEntityDescription", + "SERVICE_SELECT_FIRST", + "SERVICE_SELECT_LAST", + "SERVICE_SELECT_NEXT", + "SERVICE_SELECT_OPTION", + "SERVICE_SELECT_PREVIOUS", +] + # mypy: disallow-any-generics @@ -39,12 +65,36 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) await component.async_setup(config) + component.async_register_entity_service( + SERVICE_SELECT_FIRST, + {}, + SelectEntity.async_first.__name__, + ) + + component.async_register_entity_service( + SERVICE_SELECT_LAST, + {}, + SelectEntity.async_last.__name__, + ) + + component.async_register_entity_service( + SERVICE_SELECT_NEXT, + {vol.Optional(ATTR_CYCLE, default=True): bool}, + SelectEntity.async_next.__name__, + ) + component.async_register_entity_service( SERVICE_SELECT_OPTION, {vol.Required(ATTR_OPTION): cv.string}, async_select_option, ) + component.async_register_entity_service( + SERVICE_SELECT_PREVIOUS, + {vol.Optional(ATTR_CYCLE, default=True): bool}, + SelectEntity.async_previous.__name__, + ) + return True @@ -122,3 +172,58 @@ class SelectEntity(Entity): async def async_select_option(self, option: str) -> None: """Change the selected option.""" await self.hass.async_add_executor_job(self.select_option, option) + + @final + async def async_first(self) -> None: + """Select first option.""" + await self._async_select_index(0) + + @final + async def async_last(self) -> None: + """Select last option.""" + await self._async_select_index(-1) + + @final + async def async_next(self, cycle: bool) -> None: + """Select next option. + + If there is no current option, first item is the next. + """ + if self.current_option is None: + await self.async_first() + return + await self._async_offset_index(1, cycle) + + @final + async def async_previous(self, cycle: bool) -> None: + """Select previous option. + + If there is no current option, last item is the previous. + """ + if self.current_option is None: + await self.async_last() + return + await self._async_offset_index(-1, cycle) + + @final + async def _async_offset_index(self, offset: int, cycle: bool) -> None: + """Offset current index.""" + current_index = 0 + if self.current_option is not None and self.current_option in self.options: + current_index = self.options.index(self.current_option) + + new_index = current_index + offset + if cycle: + new_index = new_index % len(self.options) + elif new_index < 0: + new_index = 0 + elif new_index >= len(self.options): + new_index = len(self.options) - 1 + + await self.async_select_option(self.options[new_index]) + + @final + async def _async_select_index(self, idx: int) -> None: + """Select new option by index.""" + new_index = idx % len(self.options) + await self.async_select_option(self.options[new_index]) diff --git a/homeassistant/components/select/const.py b/homeassistant/components/select/const.py index 1f4615b0b05..1e2aa488e8b 100644 --- a/homeassistant/components/select/const.py +++ b/homeassistant/components/select/const.py @@ -2,9 +2,15 @@ DOMAIN = "select" +ATTR_CYCLE = "cycle" ATTR_OPTIONS = "options" ATTR_OPTION = "option" +CONF_CYCLE = "cycle" CONF_OPTION = "option" +SERVICE_SELECT_FIRST = "select_first" +SERVICE_SELECT_LAST = "select_last" +SERVICE_SELECT_NEXT = "select_next" SERVICE_SELECT_OPTION = "select_option" +SERVICE_SELECT_PREVIOUS = "select_previous" diff --git a/homeassistant/components/select/device_action.py b/homeassistant/components/select/device_action.py index 1212b9dea6b..ce1cea89c90 100644 --- a/homeassistant/components/select/device_action.py +++ b/homeassistant/components/select/device_action.py @@ -1,6 +1,8 @@ """Provides device actions for Select.""" from __future__ import annotations +from contextlib import suppress + import voluptuous as vol from homeassistant.const import ( @@ -17,16 +19,54 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import get_capability from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from .const import ATTR_OPTION, ATTR_OPTIONS, CONF_OPTION, DOMAIN, SERVICE_SELECT_OPTION +from .const import ( + ATTR_CYCLE, + ATTR_OPTION, + ATTR_OPTIONS, + CONF_CYCLE, + CONF_OPTION, + DOMAIN, + SERVICE_SELECT_FIRST, + SERVICE_SELECT_LAST, + SERVICE_SELECT_NEXT, + SERVICE_SELECT_OPTION, + SERVICE_SELECT_PREVIOUS, +) -ACTION_TYPES = {"select_option"} - -ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( - { - vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), - vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), - vol.Required(CONF_OPTION): str, - } +ACTION_SCHEMA = vol.Any( + cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): SERVICE_SELECT_FIRST, + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + } + ), + cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): SERVICE_SELECT_LAST, + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + } + ), + cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): SERVICE_SELECT_NEXT, + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + vol.Optional(CONF_CYCLE, default=True): cv.boolean, + } + ), + cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): SERVICE_SELECT_PREVIOUS, + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + vol.Optional(CONF_CYCLE, default=True): cv.boolean, + } + ), + cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): SERVICE_SELECT_OPTION, + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + vol.Required(CONF_OPTION): cv.string, + } + ), ) @@ -40,8 +80,15 @@ async def async_get_actions( CONF_DEVICE_ID: device_id, CONF_DOMAIN: DOMAIN, CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "select_option", + CONF_TYPE: service_conf_type, } + for service_conf_type in ( + SERVICE_SELECT_FIRST, + SERVICE_SELECT_LAST, + SERVICE_SELECT_NEXT, + SERVICE_SELECT_OPTION, + SERVICE_SELECT_PREVIOUS, + ) for entry in entity_registry.async_entries_for_device(registry, device_id) if entry.domain == DOMAIN ] @@ -54,13 +101,16 @@ async def async_call_action_from_config( context: Context | None, ) -> None: """Execute a device action.""" + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + if config[CONF_TYPE] == SERVICE_SELECT_OPTION: + service_data[ATTR_OPTION] = config[CONF_OPTION] + if config[CONF_TYPE] in {SERVICE_SELECT_NEXT, SERVICE_SELECT_PREVIOUS}: + service_data[ATTR_CYCLE] = config[CONF_CYCLE] + await hass.services.async_call( DOMAIN, - SERVICE_SELECT_OPTION, - { - ATTR_ENTITY_ID: config[CONF_ENTITY_ID], - ATTR_OPTION: config[CONF_OPTION], - }, + config[CONF_TYPE], + service_data, blocking=True, context=context, ) @@ -70,9 +120,19 @@ async def async_get_action_capabilities( hass: HomeAssistant, config: ConfigType ) -> dict[str, vol.Schema]: """List action capabilities.""" - try: - options = get_capability(hass, config[CONF_ENTITY_ID], ATTR_OPTIONS) or [] - except HomeAssistantError: - options = [] + if config[CONF_TYPE] in {SERVICE_SELECT_NEXT, SERVICE_SELECT_PREVIOUS}: + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_CYCLE, default=True): cv.boolean} + ) + } - return {"extra_fields": vol.Schema({vol.Required(CONF_OPTION): vol.In(options)})} + if config[CONF_TYPE] == SERVICE_SELECT_OPTION: + options: list[str] = [] + with suppress(HomeAssistantError): + options = get_capability(hass, config[CONF_ENTITY_ID], ATTR_OPTIONS) or [] + return { + "extra_fields": vol.Schema({vol.Required(CONF_OPTION): vol.In(options)}) + } + + return {} diff --git a/homeassistant/components/select/services.yaml b/homeassistant/components/select/services.yaml index edf7fb50f00..8fb55936fc9 100644 --- a/homeassistant/components/select/services.yaml +++ b/homeassistant/components/select/services.yaml @@ -1,3 +1,31 @@ +select_first: + name: First + description: Select the first option of an select entity. + target: + entity: + domain: select + +select_last: + name: Last + description: Select the last option of an select entity. + target: + entity: + domain: select + +select_next: + name: Next + description: Select the next options of an select entity. + target: + entity: + domain: select + fields: + cycle: + name: Cycle + description: If the option should cycle from the last to the first. + default: true + selector: + boolean: + select_option: name: Select description: Select an option of an select entity. @@ -12,3 +40,17 @@ select_option: example: '"Item A"' selector: text: + +select_previous: + name: Previous + description: Select the previous options of an select entity. + target: + entity: + domain: select + fields: + cycle: + name: Cycle + description: If the option should cycle from the first to the last. + default: true + selector: + boolean: diff --git a/homeassistant/components/select/strings.json b/homeassistant/components/select/strings.json index 5724ff67a14..11a4ba9517f 100644 --- a/homeassistant/components/select/strings.json +++ b/homeassistant/components/select/strings.json @@ -5,7 +5,11 @@ "current_option_changed": "{entity_name} option changed" }, "action_type": { - "select_option": "Change {entity_name} option" + "select_first": "Change {entity_name} to first option", + "select_last": "Change {entity_name} to last option", + "select_next": "Change {entity_name} to next option", + "select_option": "Change {entity_name} option", + "select_previous": "Change {entity_name} to previous option" }, "condition_type": { "selected_option": "Current {entity_name} selected option" diff --git a/tests/components/select/test_device_action.py b/tests/components/select/test_device_action.py index a3d5d1c306d..fcefa56e48e 100644 --- a/tests/components/select/test_device_action.py +++ b/tests/components/select/test_device_action.py @@ -53,11 +53,18 @@ async def test_get_actions( expected_actions = [ { "domain": DOMAIN, - "type": "select_option", + "type": action, "device_id": device_entry.id, "entity_id": "select.test_5678", "metadata": {"secondary": False}, } + for action in [ + "select_first", + "select_last", + "select_next", + "select_option", + "select_previous", + ] ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id @@ -105,7 +112,13 @@ async def test_get_actions_hidden_auxiliary( "entity_id": f"{DOMAIN}.test_5678", "metadata": {"secondary": True}, } - for action in ["select_option"] + for action in [ + "select_first", + "select_last", + "select_next", + "select_option", + "select_previous", + ] ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id @@ -113,7 +126,41 @@ async def test_get_actions_hidden_auxiliary( assert_lists_same(actions, expected_actions) -async def test_action(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("action_type", ("select_first", "select_last")) +async def test_action_select_first_last(hass: HomeAssistant, action_type: str) -> None: + """Test for select_first and select_last actions.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "select.entity", + "type": action_type, + }, + }, + ] + }, + ) + + select_calls = async_mock_service(hass, DOMAIN, action_type) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(select_calls) == 1 + assert select_calls[0].domain == DOMAIN + assert select_calls[0].service == action_type + assert select_calls[0].data == {"entity_id": "select.entity"} + + +async def test_action_select_option(hass: HomeAssistant) -> None: """Test for select_option action.""" assert await async_setup_component( hass, @@ -147,6 +194,43 @@ async def test_action(hass: HomeAssistant) -> None: assert select_calls[0].data == {"entity_id": "select.entity", "option": "option1"} +@pytest.mark.parametrize("action_type", ["select_next", "select_previous"]) +async def test_action_select_next_previous( + hass: HomeAssistant, action_type: str +) -> None: + """Test for select_next and select_previous actions.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "select.entity", + "type": action_type, + "cycle": False, + }, + }, + ] + }, + ) + + select_calls = async_mock_service(hass, DOMAIN, action_type) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(select_calls) == 1 + assert select_calls[0].domain == DOMAIN + assert select_calls[0].service == action_type + assert select_calls[0].data == {"entity_id": "select.entity", "cycle": False} + + async def test_get_action_capabilities(hass: HomeAssistant) -> None: """Test we get the expected capabilities from a select action.""" config = { @@ -189,3 +273,53 @@ async def test_get_action_capabilities(hass: HomeAssistant) -> None: "options": [("option1", "option1"), ("option2", "option2")], }, ] + + # Test next/previous actions + config = { + "platform": "device", + "domain": DOMAIN, + "type": "select_next", + "entity_id": "select.test", + } + capabilities = await async_get_action_capabilities(hass, config) + assert capabilities + assert "extra_fields" in capabilities + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "cycle", + "optional": True, + "type": "boolean", + "default": True, + }, + ] + + config["type"] = "select_previous" + capabilities = await async_get_action_capabilities(hass, config) + assert capabilities + assert "extra_fields" in capabilities + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "cycle", + "optional": True, + "type": "boolean", + "default": True, + }, + ] + + # Test action types without extra fields + config = { + "platform": "device", + "domain": DOMAIN, + "type": "select_first", + "entity_id": "select.test", + } + capabilities = await async_get_action_capabilities(hass, config) + assert capabilities == {} + + config["type"] = "select_last" + capabilities = await async_get_action_capabilities(hass, config) + assert capabilities == {} diff --git a/tests/components/select/test_init.py b/tests/components/select/test_init.py index 21745694d38..8bf870d2900 100644 --- a/tests/components/select/test_init.py +++ b/tests/components/select/test_init.py @@ -3,14 +3,19 @@ from unittest.mock import MagicMock import pytest -from homeassistant.components.select import ATTR_OPTIONS, DOMAIN, SelectEntity -from homeassistant.const import ( - ATTR_ENTITY_ID, +from homeassistant.components.select import ( + ATTR_CYCLE, ATTR_OPTION, - CONF_PLATFORM, + ATTR_OPTIONS, + DOMAIN, + SERVICE_SELECT_FIRST, + SERVICE_SELECT_LAST, + SERVICE_SELECT_NEXT, SERVICE_SELECT_OPTION, - STATE_UNKNOWN, + SERVICE_SELECT_PREVIOUS, + SelectEntity, ) +from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -41,15 +46,41 @@ async def test_select(hass: HomeAssistant) -> None: select.hass = hass + with pytest.raises(NotImplementedError): + await select.async_first() + + with pytest.raises(NotImplementedError): + await select.async_last() + + with pytest.raises(NotImplementedError): + await select.async_next(cycle=False) + + with pytest.raises(NotImplementedError): + await select.async_previous(cycle=False) + with pytest.raises(NotImplementedError): await select.async_select_option("option_one") select.select_option = MagicMock() - await select.async_select_option("option_one") + select._attr_current_option = None - assert select.select_option.called + await select.async_first() assert select.select_option.call_args[0][0] == "option_one" + await select.async_last() + assert select.select_option.call_args[0][0] == "option_three" + + await select.async_next(cycle=False) + assert select.select_option.call_args[0][0] == "option_one" + + await select.async_previous(cycle=False) + assert select.select_option.call_args[0][0] == "option_three" + + await select.async_select_option("option_two") + assert select.select_option.call_args[0][0] == "option_two" + + assert select.select_option.call_count == 5 + assert select.capability_attributes[ATTR_OPTIONS] == [ "option_one", "option_two", @@ -110,3 +141,55 @@ async def test_custom_integration_and_validation(hass, enable_custom_integration await hass.async_block_till_done() assert hass.states.get("select.select_2").state == "option 3" + + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_FIRST, + {ATTR_ENTITY_ID: "select.select_2"}, + blocking=True, + ) + assert hass.states.get("select.select_2").state == "option 1" + + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_LAST, + {ATTR_ENTITY_ID: "select.select_2"}, + blocking=True, + ) + assert hass.states.get("select.select_2").state == "option 3" + + # Do no cycle + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_NEXT, + {ATTR_ENTITY_ID: "select.select_2", ATTR_CYCLE: False}, + blocking=True, + ) + assert hass.states.get("select.select_2").state == "option 3" + + # Do cycle (default behavior) + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_NEXT, + {ATTR_ENTITY_ID: "select.select_2"}, + blocking=True, + ) + assert hass.states.get("select.select_2").state == "option 1" + + # Do not cycle + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_PREVIOUS, + {ATTR_ENTITY_ID: "select.select_2", ATTR_CYCLE: False}, + blocking=True, + ) + assert hass.states.get("select.select_2").state == "option 1" + + # Do cycle (default behavior) + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_PREVIOUS, + {ATTR_ENTITY_ID: "select.select_2"}, + blocking=True, + ) + assert hass.states.get("select.select_2").state == "option 3" diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 06285ea8cfc..bed0068cac1 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -121,35 +121,31 @@ async def test_get_actions(hass, device_ias): "metadata": {}, }, {"domain": DOMAIN, "type": "warn", "device_id": reg_device.id, "metadata": {}}, - { - "domain": Platform.SELECT, - "type": "select_option", - "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_default_siren_tone", - "metadata": {"secondary": True}, - }, - { - "domain": Platform.SELECT, - "type": "select_option", - "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_default_siren_level", - "metadata": {"secondary": True}, - }, - { - "domain": Platform.SELECT, - "type": "select_option", - "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_default_strobe_level", - "metadata": {"secondary": True}, - }, - { - "domain": Platform.SELECT, - "type": "select_option", - "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_default_strobe", - "metadata": {"secondary": True}, - }, ] + expected_actions.extend( + [ + { + "domain": Platform.SELECT, + "type": action, + "device_id": reg_device.id, + "entity_id": entity_id, + "metadata": {"secondary": True}, + } + for action in [ + "select_first", + "select_last", + "select_next", + "select_option", + "select_previous", + ] + for entity_id in [ + "select.fakemanufacturer_fakemodel_default_siren_level", + "select.fakemanufacturer_fakemodel_default_siren_tone", + "select.fakemanufacturer_fakemodel_default_strobe_level", + "select.fakemanufacturer_fakemodel_default_strobe", + ] + ] + ) assert_lists_same(actions, expected_actions)