Compare commits

...

12 Commits

Author SHA1 Message Date
epenet 7bb5483a7e Adjust 2026-05-29 14:09:46 +00:00
epenet ac94e9c8d8 Adjust 2026-05-29 14:06:36 +00:00
epenet 6c1c8135d9 docstring 2026-05-29 13:55:21 +00:00
epenet dc9a3f6a9d Reduce 2026-05-29 13:52:06 +00:00
epenet 74b88c276f Reduce 2026-05-29 13:50:34 +00:00
epenet 931b260088 Align mqtt 2026-05-29 07:12:43 +00:00
epenet fd1cce8fd9 Align google_assistant 2026-05-29 07:12:13 +00:00
epenet d24b000368 Align input_select 2026-05-29 07:10:02 +00:00
epenet 19ca4caba9 Align input_select 2026-05-29 06:53:12 +00:00
epenet 94263dca40 Tweak 2026-05-28 10:45:36 +00:00
epenet f338761f28 Tweak 2026-05-28 10:37:43 +00:00
epenet 6a9c21fc74 Add new enums to select component 2026-05-28 09:56:50 +00:00
10 changed files with 120 additions and 55 deletions
+9 -5
View File
@@ -26,6 +26,8 @@ from .const import (
SERVICE_SELECT_NEXT,
SERVICE_SELECT_OPTION,
SERVICE_SELECT_PREVIOUS,
SelectEntityAttribute,
SelectServiceArgument,
)
_LOGGER = logging.getLogger(__name__)
@@ -51,7 +53,9 @@ __all__ = [
"SERVICE_SELECT_OPTION",
"SERVICE_SELECT_PREVIOUS",
"SelectEntity",
"SelectEntityAttribute",
"SelectEntityDescription",
"SelectServiceArgument",
]
# mypy: disallow-any-generics
@@ -78,19 +82,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
component.async_register_entity_service(
SERVICE_SELECT_NEXT,
{vol.Optional(ATTR_CYCLE, default=True): bool},
{vol.Optional(SelectServiceArgument.CYCLE, default=True): bool},
SelectEntity.async_next.__name__,
)
component.async_register_entity_service(
SERVICE_SELECT_OPTION,
{vol.Required(ATTR_OPTION): cv.string},
{vol.Required(SelectServiceArgument.OPTION): cv.string},
SelectEntity.async_handle_select_option.__name__,
)
component.async_register_entity_service(
SERVICE_SELECT_PREVIOUS,
{vol.Optional(ATTR_CYCLE, default=True): bool},
{vol.Optional(SelectServiceArgument.CYCLE, default=True): bool},
SelectEntity.async_previous.__name__,
)
@@ -122,7 +126,7 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
class SelectEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Representation of a Select entity."""
_entity_component_unrecorded_attributes = frozenset({ATTR_OPTIONS})
_entity_component_unrecorded_attributes = frozenset({SelectEntityAttribute.OPTIONS})
entity_description: SelectEntityDescription
_attr_current_option: str | None = None
@@ -133,7 +137,7 @@ class SelectEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
def capability_attributes(self) -> dict[str, Any]:
"""Return capability attributes."""
return {
ATTR_OPTIONS: self.options,
SelectEntityAttribute.OPTIONS.value: self.options,
}
@property
+24 -4
View File
@@ -1,15 +1,35 @@
"""Provides the constants needed for the component."""
from enum import StrEnum
DOMAIN = "select"
ATTR_CYCLE = "cycle"
ATTR_OPTIONS = "options"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_OPTION = "option"
class SelectEntityAttribute(StrEnum):
"""Select entity attributes."""
OPTIONS = "options"
class SelectServiceArgument:
"""Select service arguments."""
CYCLE = "cycle"
OPTION = "option"
CONF_CYCLE = "cycle"
CONF_OPTION = "option"
#
# Deprecated constants
# They are single-use constants, or have been replaced by enums.
# They need to be formally deprecated when all usage is removed
# from core components
#
ATTR_CYCLE = SelectServiceArgument.CYCLE
ATTR_OPTION = SelectServiceArgument.OPTION
ATTR_OPTIONS = SelectEntityAttribute.OPTIONS.value
SERVICE_SELECT_FIRST = "select_first"
SERVICE_SELECT_LAST = "select_last"
SERVICE_SELECT_NEXT = "select_next"
@@ -22,9 +22,6 @@ from homeassistant.helpers.entity import get_capability
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from .const import (
ATTR_CYCLE,
ATTR_OPTION,
ATTR_OPTIONS,
CONF_CYCLE,
CONF_OPTION,
DOMAIN,
@@ -33,6 +30,8 @@ from .const import (
SERVICE_SELECT_NEXT,
SERVICE_SELECT_OPTION,
SERVICE_SELECT_PREVIOUS,
SelectEntityAttribute,
SelectServiceArgument,
)
_ACTION_SCHEMA = vol.Any(
@@ -112,9 +111,9 @@ async def async_call_action_from_config(
"""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]
service_data[SelectServiceArgument.OPTION] = config[CONF_OPTION]
if config[CONF_TYPE] in {SERVICE_SELECT_NEXT, SERVICE_SELECT_PREVIOUS}:
service_data[ATTR_CYCLE] = config[CONF_CYCLE]
service_data[SelectServiceArgument.CYCLE] = config[CONF_CYCLE]
await hass.services.async_call(
DOMAIN,
@@ -142,7 +141,10 @@ async def async_get_action_capabilities(
entry = async_get_entity_registry_entry_or_raise(
hass, config[CONF_ENTITY_ID]
)
options = get_capability(hass, entry.entity_id, ATTR_OPTIONS) or []
options = (
get_capability(hass, entry.entity_id, SelectEntityAttribute.OPTIONS)
or []
)
return {
"extra_fields": vol.Schema({vol.Required(CONF_OPTION): vol.In(options)})
}
@@ -24,7 +24,7 @@ from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA
from homeassistant.helpers.entity import get_capability
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from .const import ATTR_OPTIONS, CONF_OPTION, DOMAIN
from .const import CONF_OPTION, DOMAIN, SelectEntityAttribute
# nypy: disallow-any-generics
@@ -84,7 +84,9 @@ async def async_get_condition_capabilities(
try:
entry = async_get_entity_registry_entry_or_raise(hass, config[CONF_ENTITY_ID])
options = get_capability(hass, entry.entity_id, ATTR_OPTIONS) or []
options = (
get_capability(hass, entry.entity_id, SelectEntityAttribute.OPTIONS) or []
)
except HomeAssistantError:
options = []
@@ -27,7 +27,7 @@ from homeassistant.helpers.entity import get_capability
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from .const import ATTR_OPTIONS, DOMAIN
from .const import DOMAIN, SelectEntityAttribute
TRIGGER_TYPES = {"current_option_changed"}
@@ -94,7 +94,9 @@ async def async_get_trigger_capabilities(
try:
entry = async_get_entity_registry_entry_or_raise(hass, config[CONF_ENTITY_ID])
options = get_capability(hass, entry.entity_id, ATTR_OPTIONS) or []
options = (
get_capability(hass, entry.entity_id, SelectEntityAttribute.OPTIONS) or []
)
except HomeAssistantError:
options = []
@@ -8,7 +8,12 @@ from typing import Any
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import Context, HomeAssistant, State
from .const import ATTR_OPTION, ATTR_OPTIONS, DOMAIN, SERVICE_SELECT_OPTION
from .const import (
DOMAIN,
SERVICE_SELECT_OPTION,
SelectEntityAttribute,
SelectServiceArgument,
)
_LOGGER = logging.getLogger(__name__)
@@ -25,7 +30,7 @@ async def _async_reproduce_state(
_LOGGER.warning("Unable to find entity %s", state.entity_id)
return
if state.state not in cur_state.attributes.get(ATTR_OPTIONS, []):
if state.state not in cur_state.attributes.get(SelectEntityAttribute.OPTIONS, []):
_LOGGER.warning(
"Invalid state specified for %s: %s", state.entity_id, state.state
)
@@ -38,7 +43,7 @@ async def _async_reproduce_state(
await hass.services.async_call(
DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: state.entity_id, ATTR_OPTION: state.state},
{ATTR_ENTITY_ID: state.entity_id, SelectServiceArgument.OPTION: state.state},
context=context,
blocking=True,
)
+12
View File
@@ -1,5 +1,7 @@
"""Common helpers for select entity component tests."""
from enum import StrEnum
from homeassistant.components.select import SelectEntity
from tests.common import MockEntity
@@ -21,3 +23,13 @@ class MockSelectEntity(MockEntity, SelectEntity):
def select_option(self, option: str) -> None:
"""Change the selected option."""
self._values["current_option"] = option
class SelectService(StrEnum):
"""Select services."""
SELECT_FIRST = "select_first"
SELECT_LAST = "select_last"
SELECT_NEXT = "select_next"
SELECT_OPTION = "select_option"
SELECT_PREVIOUS = "select_previous"
+39 -25
View File
@@ -5,22 +5,18 @@ from unittest.mock import MagicMock
import pytest
from homeassistant.components.select import (
ATTR_CYCLE,
ATTR_OPTION,
ATTR_OPTIONS,
DOMAIN,
SERVICE_SELECT_FIRST,
SERVICE_SELECT_LAST,
SERVICE_SELECT_NEXT,
SERVICE_SELECT_OPTION,
SERVICE_SELECT_PREVIOUS,
SelectEntity,
SelectEntityAttribute,
SelectServiceArgument,
)
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.setup import async_setup_component
from .common import SelectService
from tests.common import setup_test_component_platform
@@ -85,7 +81,7 @@ async def test_select(hass: HomeAssistant) -> None:
assert select.select_option.call_count == 5
assert select.capability_attributes[ATTR_OPTIONS] == [
assert select.capability_attributes[SelectEntityAttribute.OPTIONS] == [
"option_one",
"option_two",
"option_three",
@@ -106,8 +102,11 @@ async def test_custom_integration_and_validation(
await hass.services.async_call(
DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_OPTION: "option 2", ATTR_ENTITY_ID: "select.select_1"},
SelectService.SELECT_OPTION,
{
SelectServiceArgument.OPTION: "option 2",
ATTR_ENTITY_ID: "select.select_1",
},
blocking=True,
)
@@ -119,8 +118,11 @@ async def test_custom_integration_and_validation(
with pytest.raises(ServiceValidationError) as exc:
await hass.services.async_call(
DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_OPTION: "option invalid", ATTR_ENTITY_ID: "select.select_1"},
SelectService.SELECT_OPTION,
{
SelectServiceArgument.OPTION: "option invalid",
ATTR_ENTITY_ID: "select.select_1",
},
blocking=True,
)
await hass.async_block_till_done()
@@ -134,8 +136,11 @@ async def test_custom_integration_and_validation(
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_OPTION: "option invalid", ATTR_ENTITY_ID: "select.select_2"},
SelectService.SELECT_OPTION,
{
SelectServiceArgument.OPTION: "option invalid",
ATTR_ENTITY_ID: "select.select_2",
},
blocking=True,
)
await hass.async_block_till_done()
@@ -143,8 +148,11 @@ async def test_custom_integration_and_validation(
await hass.services.async_call(
DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_OPTION: "option 3", ATTR_ENTITY_ID: "select.select_2"},
SelectService.SELECT_OPTION,
{
SelectServiceArgument.OPTION: "option 3",
ATTR_ENTITY_ID: "select.select_2",
},
blocking=True,
)
await hass.async_block_till_done()
@@ -153,7 +161,7 @@ async def test_custom_integration_and_validation(
await hass.services.async_call(
DOMAIN,
SERVICE_SELECT_FIRST,
SelectService.SELECT_FIRST,
{ATTR_ENTITY_ID: "select.select_2"},
blocking=True,
)
@@ -161,7 +169,7 @@ async def test_custom_integration_and_validation(
await hass.services.async_call(
DOMAIN,
SERVICE_SELECT_LAST,
SelectService.SELECT_LAST,
{ATTR_ENTITY_ID: "select.select_2"},
blocking=True,
)
@@ -170,8 +178,11 @@ async def test_custom_integration_and_validation(
# Do no cycle
await hass.services.async_call(
DOMAIN,
SERVICE_SELECT_NEXT,
{ATTR_ENTITY_ID: "select.select_2", ATTR_CYCLE: False},
SelectService.SELECT_NEXT,
{
ATTR_ENTITY_ID: "select.select_2",
SelectServiceArgument.CYCLE: False,
},
blocking=True,
)
assert hass.states.get("select.select_2").state == "option 3"
@@ -179,7 +190,7 @@ async def test_custom_integration_and_validation(
# Do cycle (default behavior)
await hass.services.async_call(
DOMAIN,
SERVICE_SELECT_NEXT,
SelectService.SELECT_NEXT,
{ATTR_ENTITY_ID: "select.select_2"},
blocking=True,
)
@@ -188,8 +199,11 @@ async def test_custom_integration_and_validation(
# Do not cycle
await hass.services.async_call(
DOMAIN,
SERVICE_SELECT_PREVIOUS,
{ATTR_ENTITY_ID: "select.select_2", ATTR_CYCLE: False},
SelectService.SELECT_PREVIOUS,
{
ATTR_ENTITY_ID: "select.select_2",
SelectServiceArgument.CYCLE: False,
},
blocking=True,
)
assert hass.states.get("select.select_2").state == "option 1"
@@ -197,7 +211,7 @@ async def test_custom_integration_and_validation(
# Do cycle (default behavior)
await hass.services.async_call(
DOMAIN,
SERVICE_SELECT_PREVIOUS,
SelectService.SELECT_PREVIOUS,
{ATTR_ENTITY_ID: "select.select_2"},
blocking=True,
)
+2 -2
View File
@@ -8,7 +8,7 @@ import pytest
from homeassistant.components import select
from homeassistant.components.recorder import Recorder
from homeassistant.components.recorder.history import get_significant_states
from homeassistant.components.select import ATTR_OPTIONS
from homeassistant.components.select import SelectEntityAttribute
from homeassistant.const import ATTR_FRIENDLY_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@@ -46,5 +46,5 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant)
assert len(states) >= 1
for entity_states in states.values():
for state in entity_states:
assert ATTR_OPTIONS not in state.attributes
assert SelectEntityAttribute.OPTIONS not in state.attributes
assert ATTR_FRIENDLY_NAME in state.attributes
@@ -3,15 +3,16 @@
import pytest
from homeassistant.components.select.const import (
ATTR_OPTION,
ATTR_OPTIONS,
DOMAIN,
SERVICE_SELECT_OPTION,
SelectEntityAttribute,
SelectServiceArgument,
)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.state import async_reproduce_state
from .common import SelectService
from tests.common import async_mock_service
@@ -19,11 +20,11 @@ async def test_reproducing_states(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test reproducing select states."""
calls = async_mock_service(hass, DOMAIN, SERVICE_SELECT_OPTION)
calls = async_mock_service(hass, DOMAIN, SelectService.SELECT_OPTION)
hass.states.async_set(
"select.test",
"option_one",
{ATTR_OPTIONS: ["option_one", "option_two", "option_three"]},
{SelectEntityAttribute.OPTIONS: ["option_one", "option_two", "option_three"]},
)
await async_reproduce_state(
@@ -35,7 +36,10 @@ async def test_reproducing_states(
assert len(calls) == 1
assert calls[0].domain == DOMAIN
assert calls[0].data == {ATTR_ENTITY_ID: "select.test", ATTR_OPTION: "option_two"}
assert calls[0].data == {
ATTR_ENTITY_ID: "select.test",
SelectServiceArgument.OPTION: "option_two",
}
# Calling it again should not do anything
await async_reproduce_state(