Add Select entity component platform (#51849)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Franck Nijhof 2021-06-18 11:51:55 +02:00 committed by GitHub
parent bc329cb602
commit 054ca1d7ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 345 additions and 0 deletions

View File

@ -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

View File

@ -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

View File

@ -20,6 +20,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [
"lock",
"media_player",
"number",
"select",
"sensor",
"switch",
"vacuum",

View File

@ -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()

View File

@ -0,0 +1,9 @@
{
"state": {
"demo__speed": {
"light_speed": "Light Speed",
"ludicrous_speed": "Ludicrous Speed",
"ridiculous_speed": "Ridiculous Speed"
}
}
}

View File

@ -0,0 +1,9 @@
{
"state": {
"demo__speed": {
"light_speed": "Light Speed",
"ludicrous_speed": "Ludicrous Speed",
"ridiculous_speed": "Ridiculous Speed"
}
}
}

View File

@ -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)

View File

@ -0,0 +1,8 @@
"""Provides the constants needed for the component."""
DOMAIN = "select"
ATTR_OPTIONS = "options"
ATTR_OPTION = "option"
SERVICE_SELECT_OPTION = "select_option"

View File

@ -0,0 +1,7 @@
{
"domain": "select",
"name": "Select",
"documentation": "https://www.home-assistant.io/integrations/select",
"codeowners": ["@home-assistant/core"],
"quality_scale": "internal"
}

View File

@ -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:

View File

@ -0,0 +1,3 @@
{
"title": "Select"
}

View File

@ -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

View File

@ -91,6 +91,7 @@ NO_IOT_CLASS = [
"scene",
"script",
"search",
"select",
"sensor",
"stt",
"switch",

View File

@ -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"

View File

@ -0,0 +1 @@
"""The tests for the Select integration."""

View File

@ -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