Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Component to allow selecting an option from a list as platforms."""
2 
3 from __future__ import annotations
4 
5 from datetime import timedelta
6 import logging
7 from typing import Any, final
8 
9 from propcache import cached_property
10 import voluptuous as vol
11 
12 from homeassistant.config_entries import ConfigEntry
13 from homeassistant.core import HomeAssistant, callback
14 from homeassistant.exceptions import ServiceValidationError
15 from homeassistant.helpers import config_validation as cv
16 from homeassistant.helpers.entity import Entity, EntityDescription
17 from homeassistant.helpers.entity_component import EntityComponent
18 from homeassistant.helpers.typing import ConfigType
19 from homeassistant.util.hass_dict import HassKey
20 
21 from .const import (
22  ATTR_CYCLE,
23  ATTR_OPTION,
24  ATTR_OPTIONS,
25  DOMAIN,
26  SERVICE_SELECT_FIRST,
27  SERVICE_SELECT_LAST,
28  SERVICE_SELECT_NEXT,
29  SERVICE_SELECT_OPTION,
30  SERVICE_SELECT_PREVIOUS,
31 )
32 
33 _LOGGER = logging.getLogger(__name__)
34 
35 DATA_COMPONENT: HassKey[EntityComponent[SelectEntity]] = HassKey(DOMAIN)
36 ENTITY_ID_FORMAT = DOMAIN + ".{}"
37 PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
38 PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
39 SCAN_INTERVAL = timedelta(seconds=30)
40 
41 MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
42 
43 __all__ = [
44  "ATTR_CYCLE",
45  "ATTR_OPTION",
46  "ATTR_OPTIONS",
47  "DOMAIN",
48  "PLATFORM_SCHEMA_BASE",
49  "PLATFORM_SCHEMA",
50  "SelectEntity",
51  "SelectEntityDescription",
52  "SERVICE_SELECT_FIRST",
53  "SERVICE_SELECT_LAST",
54  "SERVICE_SELECT_NEXT",
55  "SERVICE_SELECT_OPTION",
56  "SERVICE_SELECT_PREVIOUS",
57 ]
58 
59 # mypy: disallow-any-generics
60 
61 
62 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
63  """Set up Select entities."""
64  component = hass.data[DATA_COMPONENT] = EntityComponent[SelectEntity](
65  _LOGGER, DOMAIN, hass, SCAN_INTERVAL
66  )
67  await component.async_setup(config)
68 
69  component.async_register_entity_service(
70  SERVICE_SELECT_FIRST,
71  None,
72  SelectEntity.async_first.__name__,
73  )
74 
75  component.async_register_entity_service(
76  SERVICE_SELECT_LAST,
77  None,
78  SelectEntity.async_last.__name__,
79  )
80 
81  component.async_register_entity_service(
82  SERVICE_SELECT_NEXT,
83  {vol.Optional(ATTR_CYCLE, default=True): bool},
84  SelectEntity.async_next.__name__,
85  )
86 
87  component.async_register_entity_service(
88  SERVICE_SELECT_OPTION,
89  {vol.Required(ATTR_OPTION): cv.string},
90  SelectEntity.async_handle_select_option.__name__,
91  )
92 
93  component.async_register_entity_service(
94  SERVICE_SELECT_PREVIOUS,
95  {vol.Optional(ATTR_CYCLE, default=True): bool},
96  SelectEntity.async_previous.__name__,
97  )
98 
99  return True
100 
101 
102 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
103  """Set up a config entry."""
104  return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
105 
106 
107 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
108  """Unload a config entry."""
109  return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
110 
111 
112 class SelectEntityDescription(EntityDescription, frozen_or_thawed=True):
113  """A class that describes select entities."""
114 
115  options: list[str] | None = None
116 
117 
118 CACHED_PROPERTIES_WITH_ATTR_ = {
119  "current_option",
120  "options",
121 }
122 
123 
124 class SelectEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
125  """Representation of a Select entity."""
126 
127  _entity_component_unrecorded_attributes = frozenset({ATTR_OPTIONS})
128 
129  entity_description: SelectEntityDescription
130  _attr_current_option: str | None
131  _attr_options: list[str]
132  _attr_state: None = None
133 
134  @property
135  def capability_attributes(self) -> dict[str, Any]:
136  """Return capability attributes."""
137  return {
138  ATTR_OPTIONS: self.optionsoptions,
139  }
140 
141  @property
142  @final
143  def state(self) -> str | None:
144  """Return the entity state."""
145  current_option = self.current_optioncurrent_option
146  if current_option is None or current_option not in self.optionsoptions:
147  return None
148  return current_option
149 
150  @cached_property
151  def options(self) -> list[str]:
152  """Return a set of selectable options."""
153  if hasattr(self, "_attr_options"):
154  return self._attr_options
155  if (
156  hasattr(self, "entity_description")
157  and self.entity_description.options is not None
158  ):
159  return self.entity_description.options
160  raise AttributeError
161 
162  @cached_property
163  def current_option(self) -> str | None:
164  """Return the selected entity option to represent the entity state."""
165  return self._attr_current_option
166 
167  @final
168  @callback
169  def _valid_option_or_raise(self, option: str) -> None:
170  """Raise ServiceValidationError on invalid option."""
171  options = self.optionsoptions
172  if not options or option not in options:
173  friendly_options: str = ", ".join(options or [])
175  translation_domain=DOMAIN,
176  translation_key="not_valid_option",
177  translation_placeholders={
178  "entity_id": self.entity_identity_id,
179  "option": option,
180  "options": friendly_options,
181  },
182  )
183 
184  @final
185  async def async_handle_select_option(self, option: str) -> None:
186  """Service call wrapper to set a new value."""
187  self._valid_option_or_raise_valid_option_or_raise(option)
188  await self.async_select_optionasync_select_option(option)
189 
190  def select_option(self, option: str) -> None:
191  """Change the selected option."""
192  raise NotImplementedError
193 
194  async def async_select_option(self, option: str) -> None:
195  """Change the selected option."""
196  await self.hasshass.async_add_executor_job(self.select_optionselect_option, option)
197 
198  @final
199  async def async_first(self) -> None:
200  """Select first option."""
201  await self._async_select_index_async_select_index(0)
202 
203  @final
204  async def async_last(self) -> None:
205  """Select last option."""
206  await self._async_select_index_async_select_index(-1)
207 
208  @final
209  async def async_next(self, cycle: bool) -> None:
210  """Select next option.
211 
212  If there is no current option, first item is the next.
213  """
214  if self.current_optioncurrent_option is None:
215  await self.async_firstasync_first()
216  return
217  await self._async_offset_index_async_offset_index(1, cycle)
218 
219  @final
220  async def async_previous(self, cycle: bool) -> None:
221  """Select previous option.
222 
223  If there is no current option, last item is the previous.
224  """
225  if self.current_optioncurrent_option is None:
226  await self.async_lastasync_last()
227  return
228  await self._async_offset_index_async_offset_index(-1, cycle)
229 
230  @final
231  async def _async_offset_index(self, offset: int, cycle: bool) -> None:
232  """Offset current index."""
233  current_index = 0
234  current_option = self.current_optioncurrent_option
235  options = self.optionsoptions
236  if current_option is not None and current_option in self.optionsoptions:
237  current_index = self.optionsoptions.index(current_option)
238 
239  new_index = current_index + offset
240  if cycle:
241  new_index = new_index % len(options)
242  elif new_index < 0:
243  new_index = 0
244  elif new_index >= len(options):
245  new_index = len(options) - 1
246 
247  await self.async_select_optionasync_select_option(options[new_index])
248 
249  @final
250  async def _async_select_index(self, idx: int) -> None:
251  """Select new option by index."""
252  options = self.optionsoptions
253  new_index = idx % len(options)
254  await self.async_select_optionasync_select_option(options[new_index])
None _valid_option_or_raise(self, str option)
Definition: __init__.py:169
dict[str, Any] capability_attributes(self)
Definition: __init__.py:135
None _async_offset_index(self, int offset, bool cycle)
Definition: __init__.py:231
None async_handle_select_option(self, str option)
Definition: __init__.py:185
None async_select_option(self, str option)
Definition: __init__.py:194
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:107
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:62
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:102