Add Select entity component platform (#51849)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>pull/51977/head
parent
bc329cb602
commit
054ca1d7ec
|
@ -61,6 +61,7 @@ homeassistant.components.recorder.repack
|
|||
homeassistant.components.recorder.statistics
|
||||
homeassistant.components.remote.*
|
||||
homeassistant.components.scene.*
|
||||
homeassistant.components.select.*
|
||||
homeassistant.components.sensor.*
|
||||
homeassistant.components.slack.*
|
||||
homeassistant.components.sonos.media_player
|
||||
|
|
|
@ -423,6 +423,7 @@ homeassistant/components/scrape/* @fabaff
|
|||
homeassistant/components/screenlogic/* @dieselrabbit
|
||||
homeassistant/components/script/* @home-assistant/core
|
||||
homeassistant/components/search/* @home-assistant/core
|
||||
homeassistant/components/select/* @home-assistant/core
|
||||
homeassistant/components/sense/* @kbickar
|
||||
homeassistant/components/sensibo/* @andrey-git
|
||||
homeassistant/components/sentry/* @dcramer @frenck
|
||||
|
|
|
@ -20,6 +20,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [
|
|||
"lock",
|
||||
"media_player",
|
||||
"number",
|
||||
"select",
|
||||
"sensor",
|
||||
"switch",
|
||||
"vacuum",
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
"""Demo platform that offers a fake select entity."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import DEVICE_DEFAULT_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType = None,
|
||||
) -> None:
|
||||
"""Set up the demo Select entity."""
|
||||
async_add_entities(
|
||||
[
|
||||
DemoSelect(
|
||||
unique_id="speed",
|
||||
name="Speed",
|
||||
icon="mdi:speedometer",
|
||||
device_class="demo__speed",
|
||||
current_option="ridiculous_speed",
|
||||
options=[
|
||||
"light_speed",
|
||||
"ridiculous_speed",
|
||||
"ludicrous_speed",
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Demo config entry."""
|
||||
await async_setup_platform(hass, {}, async_add_entities)
|
||||
|
||||
|
||||
class DemoSelect(SelectEntity):
|
||||
"""Representation of a demo select entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unique_id: str,
|
||||
name: str,
|
||||
icon: str,
|
||||
device_class: str | None,
|
||||
current_option: str | None,
|
||||
options: list[str],
|
||||
) -> None:
|
||||
"""Initialize the Demo select entity."""
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_name = name or DEVICE_DEFAULT_NAME
|
||||
self._attr_current_option = current_option
|
||||
self._attr_icon = icon
|
||||
self._attr_device_class = device_class
|
||||
self._attr_options = options
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(DOMAIN, unique_id)},
|
||||
"name": name,
|
||||
}
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Update the current selected option."""
|
||||
if option not in self.options:
|
||||
raise ValueError(f"Invalid option for {self.entity_id}: {option}")
|
||||
|
||||
self._attr_current_option = option
|
||||
self.async_write_ha_state()
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"state": {
|
||||
"demo__speed": {
|
||||
"light_speed": "Light Speed",
|
||||
"ludicrous_speed": "Ludicrous Speed",
|
||||
"ridiculous_speed": "Ridiculous Speed"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"state": {
|
||||
"demo__speed": {
|
||||
"light_speed": "Light Speed",
|
||||
"ludicrous_speed": "Ludicrous Speed",
|
||||
"ridiculous_speed": "Ridiculous Speed"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
"""Component to allow selecting an option from a list as platforms."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, final
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.config_validation import ( # noqa: F401
|
||||
PLATFORM_SCHEMA,
|
||||
PLATFORM_SCHEMA_BASE,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import ATTR_OPTION, ATTR_OPTIONS, DOMAIN, SERVICE_SELECT_OPTION
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Select entities."""
|
||||
component = hass.data[DOMAIN] = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
||||
)
|
||||
await component.async_setup(config)
|
||||
|
||||
component.async_register_entity_service(
|
||||
SERVICE_SELECT_OPTION,
|
||||
{vol.Required(ATTR_OPTION): cv.string},
|
||||
"async_select_option",
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a config entry."""
|
||||
component: EntityComponent = hass.data[DOMAIN]
|
||||
return await component.async_setup_entry(entry)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
component: EntityComponent = hass.data[DOMAIN]
|
||||
return await component.async_unload_entry(entry)
|
||||
|
||||
|
||||
class SelectEntity(Entity):
|
||||
"""Representation of a Select entity."""
|
||||
|
||||
_attr_current_option: str | None
|
||||
_attr_options: list[str]
|
||||
_attr_state: None = None
|
||||
|
||||
@property
|
||||
def capability_attributes(self) -> dict[str, Any]:
|
||||
"""Return capability attributes."""
|
||||
return {
|
||||
ATTR_OPTIONS: self.options,
|
||||
}
|
||||
|
||||
@property
|
||||
@final
|
||||
def state(self) -> str | None:
|
||||
"""Return the entity state."""
|
||||
if self.current_option is None or self.current_option not in self.options:
|
||||
return None
|
||||
return self.current_option
|
||||
|
||||
@property
|
||||
def options(self) -> list[str]:
|
||||
"""Return a set of selectable options."""
|
||||
return self._attr_options
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the selected entity option to represent the entity state."""
|
||||
return self._attr_current_option
|
||||
|
||||
def select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
await self.hass.async_add_executor_job(self.select_option, option)
|
|
@ -0,0 +1,8 @@
|
|||
"""Provides the constants needed for the component."""
|
||||
|
||||
DOMAIN = "select"
|
||||
|
||||
ATTR_OPTIONS = "options"
|
||||
ATTR_OPTION = "option"
|
||||
|
||||
SERVICE_SELECT_OPTION = "select_option"
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"domain": "select",
|
||||
"name": "Select",
|
||||
"documentation": "https://www.home-assistant.io/integrations/select",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"quality_scale": "internal"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
select_option:
|
||||
name: Select
|
||||
description: Select an option of an select entity.
|
||||
target:
|
||||
entity:
|
||||
domain: select
|
||||
fields:
|
||||
option:
|
||||
name: Option
|
||||
description: Option to be selected.
|
||||
required: true
|
||||
example: '"Item A"'
|
||||
selector:
|
||||
text:
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"title": "Select"
|
||||
}
|
11
mypy.ini
11
mypy.ini
|
@ -682,6 +682,17 @@ no_implicit_optional = true
|
|||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.select.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.sensor.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
|
|
@ -91,6 +91,7 @@ NO_IOT_CLASS = [
|
|||
"scene",
|
||||
"script",
|
||||
"search",
|
||||
"select",
|
||||
"sensor",
|
||||
"stt",
|
||||
"switch",
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
"""The tests for the demo select component."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.select.const import (
|
||||
ATTR_OPTION,
|
||||
ATTR_OPTIONS,
|
||||
DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
ENTITY_SPEED = "select.speed"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_demo_select(hass: HomeAssistant) -> None:
|
||||
"""Initialize setup demo select entity."""
|
||||
assert await async_setup_component(hass, DOMAIN, {"select": {"platform": "demo"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
def test_setup_params(hass: HomeAssistant) -> None:
|
||||
"""Test the initial parameters."""
|
||||
state = hass.states.get(ENTITY_SPEED)
|
||||
assert state
|
||||
assert state.state == "ridiculous_speed"
|
||||
assert state.attributes.get(ATTR_OPTIONS) == [
|
||||
"light_speed",
|
||||
"ridiculous_speed",
|
||||
"ludicrous_speed",
|
||||
]
|
||||
|
||||
|
||||
async def test_select_option_bad_attr(hass: HomeAssistant) -> None:
|
||||
"""Test selecting a different option with invalid option value."""
|
||||
state = hass.states.get(ENTITY_SPEED)
|
||||
assert state
|
||||
assert state.state == "ridiculous_speed"
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_OPTION: "slow_speed", ATTR_ENTITY_ID: ENTITY_SPEED},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_SPEED)
|
||||
assert state
|
||||
assert state.state == "ridiculous_speed"
|
||||
|
||||
|
||||
async def test_select_option(hass: HomeAssistant) -> None:
|
||||
"""Test selecting of a option."""
|
||||
state = hass.states.get(ENTITY_SPEED)
|
||||
assert state
|
||||
assert state.state == "ridiculous_speed"
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_OPTION: "light_speed", ATTR_ENTITY_ID: ENTITY_SPEED},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_SPEED)
|
||||
assert state
|
||||
assert state.state == "light_speed"
|
|
@ -0,0 +1 @@
|
|||
"""The tests for the Select integration."""
|
|
@ -0,0 +1,28 @@
|
|||
"""The tests for the Select component."""
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
class MockSelectEntity(SelectEntity):
|
||||
"""Mock SelectEntity to use in tests."""
|
||||
|
||||
_attr_current_option = "option_one"
|
||||
_attr_options = ["option_one", "option_two", "option_three"]
|
||||
|
||||
|
||||
async def test_select(hass: HomeAssistant) -> None:
|
||||
"""Test getting data from the mocked select entity."""
|
||||
select = MockSelectEntity()
|
||||
assert select.current_option == "option_one"
|
||||
assert select.state == "option_one"
|
||||
assert select.options == ["option_one", "option_two", "option_three"]
|
||||
|
||||
# Test none selected
|
||||
select._attr_current_option = None
|
||||
assert select.current_option is None
|
||||
assert select.state is None
|
||||
|
||||
# Test none existing selected
|
||||
select._attr_current_option = "option_four"
|
||||
assert select.current_option == "option_four"
|
||||
assert select.state is None
|
Loading…
Reference in New Issue