Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Component to allow setting text as platforms."""
2 
3 from __future__ import annotations
4 
5 from dataclasses import asdict, dataclass
6 from datetime import timedelta
7 from enum import StrEnum
8 import logging
9 import re
10 from typing import Any, final
11 
12 from propcache import cached_property
13 import voluptuous as vol
14 
15 from homeassistant.config_entries import ConfigEntry
16 from homeassistant.const import MAX_LENGTH_STATE_STATE
17 from homeassistant.core import HomeAssistant, ServiceCall
18 from homeassistant.helpers import config_validation as cv
19 from homeassistant.helpers.entity import Entity, EntityDescription
20 from homeassistant.helpers.entity_component import EntityComponent
21 from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
22 from homeassistant.helpers.typing import ConfigType
23 from homeassistant.util.hass_dict import HassKey
24 
25 from .const import (
26  ATTR_MAX,
27  ATTR_MIN,
28  ATTR_MODE,
29  ATTR_PATTERN,
30  ATTR_VALUE,
31  DOMAIN,
32  SERVICE_SET_VALUE,
33 )
34 
35 _LOGGER = logging.getLogger(__name__)
36 
37 DATA_COMPONENT: HassKey[EntityComponent[TextEntity]] = HassKey(DOMAIN)
38 ENTITY_ID_FORMAT = DOMAIN + ".{}"
39 PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
40 PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
41 SCAN_INTERVAL = timedelta(seconds=30)
42 
43 MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
44 
45 
46 __all__ = ["DOMAIN", "TextEntity", "TextEntityDescription", "TextMode"]
47 
48 
49 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
50  """Set up Text entities."""
51  component = hass.data[DATA_COMPONENT] = EntityComponent[TextEntity](
52  _LOGGER, DOMAIN, hass, SCAN_INTERVAL
53  )
54  await component.async_setup(config)
55 
56  component.async_register_entity_service(
57  SERVICE_SET_VALUE,
58  {vol.Required(ATTR_VALUE): cv.string},
59  _async_set_value,
60  )
61 
62  return True
63 
64 
65 async def _async_set_value(entity: TextEntity, service_call: ServiceCall) -> None:
66  """Service call wrapper to set a new value."""
67  value = service_call.data[ATTR_VALUE]
68  if len(value) < entity.min:
69  raise ValueError(
70  f"Value {value} for {entity.entity_id} is too short (minimum length"
71  f" {entity.min})"
72  )
73  if len(value) > entity.max:
74  raise ValueError(
75  f"Value {value} for {entity.entity_id} is too long (maximum length {entity.max})"
76  )
77  if entity.pattern_cmp and not entity.pattern_cmp.match(value):
78  raise ValueError(
79  f"Value {value} for {entity.entity_id} doesn't match pattern {entity.pattern}"
80  )
81  await entity.async_set_value(value)
82 
83 
84 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
85  """Set up a config entry."""
86  return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
87 
88 
89 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
90  """Unload a config entry."""
91  return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
92 
93 
94 class TextMode(StrEnum):
95  """Modes for text entities."""
96 
97  PASSWORD = "password"
98  TEXT = "text"
99 
100 
101 class TextEntityDescription(EntityDescription, frozen_or_thawed=True):
102  """A class that describes text entities."""
103 
104  native_min: int = 0
105  native_max: int = MAX_LENGTH_STATE_STATE
106  mode: TextMode = TextMode.TEXT
107  pattern: str | None = None
108 
109 
110 CACHED_PROPERTIES_WITH_ATTR_ = {
111  "mode",
112  "native_value",
113  "native_min",
114  "native_max",
115  "pattern",
116 }
117 
118 
119 class TextEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
120  """Representation of a Text entity."""
121 
122  _entity_component_unrecorded_attributes = frozenset(
123  {ATTR_MAX, ATTR_MIN, ATTR_MODE, ATTR_PATTERN}
124  )
125 
126  entity_description: TextEntityDescription
127  _attr_mode: TextMode
128  _attr_native_value: str | None
129  _attr_native_min: int
130  _attr_native_max: int
131  _attr_pattern: str | None
132  _attr_state: None = None
133  __pattern_cmp: re.Pattern | None = None
134 
135  @property
136  def capability_attributes(self) -> dict[str, Any]:
137  """Return capability attributes."""
138  return {
139  ATTR_MODE: self.modemode,
140  ATTR_MIN: self.minmin,
141  ATTR_MAX: self.maxmax,
142  ATTR_PATTERN: self.patternpattern,
143  }
144 
145  @property
146  @final
147  def state(self) -> str | None:
148  """Return the entity state."""
149  if self.native_valuenative_value is None:
150  return None
151  if len(self.native_valuenative_value) < self.minmin:
152  raise ValueError(
153  f"Entity {self.entity_id} provides state {self.native_value} which is "
154  f"too short (minimum length {self.min})"
155  )
156  if len(self.native_valuenative_value) > self.maxmax:
157  raise ValueError(
158  f"Entity {self.entity_id} provides state {self.native_value} which is "
159  f"too long (maximum length {self.max})"
160  )
161  if self.pattern_cmppattern_cmp and not self.pattern_cmppattern_cmp.match(self.native_valuenative_value):
162  raise ValueError(
163  f"Entity {self.entity_id} provides state {self.native_value} which "
164  f"does not match expected pattern {self.pattern}"
165  )
166  return self.native_valuenative_value
167 
168  @cached_property
169  def mode(self) -> TextMode:
170  """Return the mode of the entity."""
171  if hasattr(self, "_attr_mode"):
172  return self._attr_mode
173  if hasattr(self, "entity_description"):
174  return self.entity_description.mode
175  return TextMode.TEXT
176 
177  @cached_property
178  def native_min(self) -> int:
179  """Return the minimum length of the value."""
180  if hasattr(self, "_attr_native_min"):
181  return self._attr_native_min
182  if hasattr(self, "entity_description"):
183  return self.entity_description.native_min
184  return 0
185 
186  @property
187  @final
188  def min(self) -> int:
189  """Return the minimum length of the value."""
190  return max(self.native_minnative_min, 0)
191 
192  @cached_property
193  def native_max(self) -> int:
194  """Return the maximum length of the value."""
195  if hasattr(self, "_attr_native_max"):
196  return self._attr_native_max
197  if hasattr(self, "entity_description"):
198  return self.entity_description.native_max
199  return MAX_LENGTH_STATE_STATE
200 
201  @property
202  @final
203  def max(self) -> int:
204  """Return the maximum length of the value."""
205  return min(self.native_maxnative_max, MAX_LENGTH_STATE_STATE)
206 
207  @property
208  @final
209  def pattern_cmp(self) -> re.Pattern | None:
210  """Return a compiled pattern."""
211  if self.patternpattern is None:
212  self.__pattern_cmp__pattern_cmp = None
213  return None
214  if not self.__pattern_cmp__pattern_cmp or self.patternpattern != self.__pattern_cmp__pattern_cmp.pattern:
215  self.__pattern_cmp__pattern_cmp = re.compile(self.patternpattern)
216  return self.__pattern_cmp__pattern_cmp
217 
218  @cached_property
219  def pattern(self) -> str | None:
220  """Return the regex pattern that the value must match."""
221  if hasattr(self, "_attr_pattern"):
222  return self._attr_pattern
223  if hasattr(self, "entity_description"):
224  return self.entity_description.pattern
225  return None
226 
227  @cached_property
228  def native_value(self) -> str | None:
229  """Return the value reported by the text."""
230  return self._attr_native_value
231 
232  def set_value(self, value: str) -> None:
233  """Change the value."""
234  raise NotImplementedError
235 
236  async def async_set_value(self, value: str) -> None:
237  """Change the value."""
238  await self.hasshass.async_add_executor_job(self.set_valueset_value, value)
239 
240 
241 @dataclass
243  """Object to hold extra stored data."""
244 
245  native_value: str | None
246  native_min: int
247  native_max: int
248 
249  def as_dict(self) -> dict[str, Any]:
250  """Return a dict representation of the text data."""
251  return asdict(self)
252 
253  @classmethod
254  def from_dict(cls, restored: dict[str, Any]) -> TextExtraStoredData | None:
255  """Initialize a stored text state from a dict."""
256  try:
257  return cls(
258  restored["native_value"],
259  restored["native_min"],
260  restored["native_max"],
261  )
262  except KeyError:
263  return None
264 
265 
267  """Mixin class for restoring previous text state."""
268 
269  @property
270  def extra_restore_state_data(self) -> TextExtraStoredData:
271  """Return text specific state data to be restored."""
272  return TextExtraStoredData(
273  self.native_valuenative_value,
274  self.native_minnative_min,
275  self.native_maxnative_max,
276  )
277 
278  async def async_get_last_text_data(self) -> TextExtraStoredData | None:
279  """Restore attributes."""
280  if (restored_last_extra_data := await self.async_get_last_extra_dataasync_get_last_extra_data()) is None:
281  return None
282  return TextExtraStoredData.from_dict(restored_last_extra_data.as_dict())
TextExtraStoredData extra_restore_state_data(self)
Definition: __init__.py:270
TextExtraStoredData|None async_get_last_text_data(self)
Definition: __init__.py:278
re.Pattern|None pattern_cmp(self)
Definition: __init__.py:209
None async_set_value(self, str value)
Definition: __init__.py:236
dict[str, Any] capability_attributes(self)
Definition: __init__.py:136
TextExtraStoredData|None from_dict(cls, dict[str, Any] restored)
Definition: __init__.py:254
ExtraStoredData|None async_get_last_extra_data(self)
list[_T] match(self, BluetoothServiceInfoBleak service_info)
Definition: match.py:246
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:84
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:89
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:49
None _async_set_value(TextEntity entity, ServiceCall service_call)
Definition: __init__.py:65