Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support to enter a value into a text box."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any, Self
7 
8 import voluptuous as vol
9 
10 from homeassistant.const import (
11  ATTR_EDITABLE,
12  ATTR_MODE,
13  CONF_ICON,
14  CONF_ID,
15  CONF_MODE,
16  CONF_NAME,
17  CONF_UNIT_OF_MEASUREMENT,
18  SERVICE_RELOAD,
19 )
20 from homeassistant.core import HomeAssistant, ServiceCall, callback
21 from homeassistant.helpers import collection
23 from homeassistant.helpers.entity_component import EntityComponent
24 from homeassistant.helpers.restore_state import RestoreEntity
26 from homeassistant.helpers.storage import Store
27 from homeassistant.helpers.typing import ConfigType, VolDictType
28 
29 _LOGGER = logging.getLogger(__name__)
30 
31 DOMAIN = "input_text"
32 
33 CONF_INITIAL = "initial"
34 CONF_MIN = "min"
35 CONF_MIN_VALUE = 0
36 CONF_MAX = "max"
37 CONF_MAX_VALUE = 100
38 CONF_PATTERN = "pattern"
39 CONF_VALUE = "value"
40 
41 MODE_TEXT = "text"
42 MODE_PASSWORD = "password"
43 
44 ATTR_VALUE = CONF_VALUE
45 ATTR_MIN = "min"
46 ATTR_MAX = "max"
47 ATTR_PATTERN = CONF_PATTERN
48 
49 SERVICE_SET_VALUE = "set_value"
50 STORAGE_KEY = DOMAIN
51 STORAGE_VERSION = 1
52 
53 STORAGE_FIELDS: VolDictType = {
54  vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
55  vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int),
56  vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int),
57  vol.Optional(CONF_INITIAL, ""): cv.string,
58  vol.Optional(CONF_ICON): cv.icon,
59  vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
60  vol.Optional(CONF_PATTERN): cv.string,
61  vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In([MODE_TEXT, MODE_PASSWORD]),
62 }
63 
64 
65 def _cv_input_text(config: dict[str, Any]) -> dict[str, Any]:
66  """Configure validation helper for input box (voluptuous)."""
67  minimum: int = config[CONF_MIN]
68  maximum: int = config[CONF_MAX]
69  if minimum > maximum:
70  raise vol.Invalid(
71  f"Max len ({minimum}) is not greater than min len ({maximum})"
72  )
73  state: str | None = config.get(CONF_INITIAL)
74  if state is not None and (len(state) < minimum or len(state) > maximum):
75  raise vol.Invalid(
76  f"Initial value {state} length not in range {minimum}-{maximum}"
77  )
78  return config
79 
80 
81 CONFIG_SCHEMA = vol.Schema(
82  {
83  DOMAIN: cv.schema_with_slug_keys(
84  vol.All(
85  lambda value: value or {},
86  {
87  vol.Optional(CONF_NAME): cv.string,
88  vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int),
89  vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int),
90  vol.Optional(CONF_INITIAL): cv.string,
91  vol.Optional(CONF_ICON): cv.icon,
92  vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
93  vol.Optional(CONF_PATTERN): cv.string,
94  vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In(
95  [MODE_TEXT, MODE_PASSWORD]
96  ),
97  },
98  _cv_input_text,
99  ),
100  )
101  },
102  extra=vol.ALLOW_EXTRA,
103 )
104 RELOAD_SERVICE_SCHEMA = vol.Schema({})
105 
106 
107 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
108  """Set up an input text."""
109  component = EntityComponent[InputText](_LOGGER, DOMAIN, hass)
110 
111  id_manager = collection.IDManager()
112 
113  yaml_collection = collection.YamlCollection(
114  logging.getLogger(f"{__name__}.yaml_collection"), id_manager
115  )
116  collection.sync_entity_lifecycle(
117  hass, DOMAIN, DOMAIN, component, yaml_collection, InputText
118  )
119 
120  storage_collection = InputTextStorageCollection(
121  Store(hass, STORAGE_VERSION, STORAGE_KEY),
122  id_manager,
123  )
124  collection.sync_entity_lifecycle(
125  hass, DOMAIN, DOMAIN, component, storage_collection, InputText
126  )
127 
128  await yaml_collection.async_load(
129  [{CONF_ID: id_, **(conf or {})} for id_, conf in config.get(DOMAIN, {}).items()]
130  )
131  await storage_collection.async_load()
132 
133  collection.DictStorageCollectionWebsocket(
134  storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
135  ).async_setup(hass)
136 
137  async def reload_service_handler(service_call: ServiceCall) -> None:
138  """Reload yaml entities."""
139  conf = await component.async_prepare_reload(skip_reset=True)
140  if conf is None:
141  conf = {DOMAIN: {}}
142  await yaml_collection.async_load(
143  [{CONF_ID: id_, **(cfg or {})} for id_, cfg in conf.get(DOMAIN, {}).items()]
144  )
145 
147  hass,
148  DOMAIN,
149  SERVICE_RELOAD,
150  reload_service_handler,
151  schema=RELOAD_SERVICE_SCHEMA,
152  )
153 
154  component.async_register_entity_service(
155  SERVICE_SET_VALUE, {vol.Required(ATTR_VALUE): cv.string}, "async_set_value"
156  )
157 
158  return True
159 
160 
161 class InputTextStorageCollection(collection.DictStorageCollection):
162  """Input storage based collection."""
163 
164  CREATE_UPDATE_SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, _cv_input_text))
165 
166  async def _process_create_data(self, data: dict[str, Any]) -> dict[str, Any]:
167  """Validate the config is valid."""
168  return self.CREATE_UPDATE_SCHEMACREATE_UPDATE_SCHEMA(data) # type: ignore[no-any-return]
169 
170  @callback
171  def _get_suggested_id(self, info: dict[str, Any]) -> str:
172  """Suggest an ID based on the config."""
173  return info[CONF_NAME] # type: ignore[no-any-return]
174 
175  async def _update_data(
176  self, item: dict[str, Any], update_data: dict[str, Any]
177  ) -> dict[str, Any]:
178  """Return a new updated data object."""
179  update_data = self.CREATE_UPDATE_SCHEMACREATE_UPDATE_SCHEMA(update_data)
180  return {CONF_ID: item[CONF_ID]} | update_data
181 
182 
183 class InputText(collection.CollectionEntity, RestoreEntity):
184  """Represent a text box."""
185 
186  _unrecorded_attributes = frozenset(
187  {ATTR_EDITABLE, ATTR_MAX, ATTR_MIN, ATTR_MODE, ATTR_PATTERN}
188  )
189 
190  _attr_should_poll = False
191  _current_value: str | None
192  editable: bool
193 
194  def __init__(self, config: ConfigType) -> None:
195  """Initialize a text input."""
196  self._config_config = config
197  self._current_value_current_value = config.get(CONF_INITIAL)
198 
199  @classmethod
200  def from_storage(cls, config: ConfigType) -> Self:
201  """Return entity instance initialized from storage."""
202  input_text: Self = cls(config)
203  input_text.editable = True
204  return input_text
205 
206  @classmethod
207  def from_yaml(cls, config: ConfigType) -> Self:
208  """Return entity instance initialized from yaml."""
209  input_text: Self = cls(config)
210  input_text.entity_id = f"{DOMAIN}.{config[CONF_ID]}"
211  input_text.editable = False
212  return input_text
213 
214  @property
215  def name(self) -> str | None:
216  """Return the name of the text input entity."""
217  return self._config_config.get(CONF_NAME)
218 
219  @property
220  def icon(self) -> str | None:
221  """Return the icon to be used for this entity."""
222  return self._config_config.get(CONF_ICON)
223 
224  @property
225  def _maximum(self) -> int:
226  """Return max len of the text."""
227  return self._config_config[CONF_MAX] # type: ignore[no-any-return]
228 
229  @property
230  def _minimum(self) -> int:
231  """Return min len of the text."""
232  return self._config_config[CONF_MIN] # type: ignore[no-any-return]
233 
234  @property
235  def state(self) -> str | None:
236  """Return the state of the component."""
237  return self._current_value_current_value
238 
239  @property
240  def unit_of_measurement(self) -> str | None:
241  """Return the unit the value is expressed in."""
242  return self._config_config.get(CONF_UNIT_OF_MEASUREMENT)
243 
244  @property
245  def unique_id(self) -> str:
246  """Return unique id for the entity."""
247  return self._config_config[CONF_ID] # type: ignore[no-any-return]
248 
249  @property
250  def extra_state_attributes(self) -> dict[str, Any]:
251  """Return the state attributes."""
252  return {
253  ATTR_EDITABLE: self.editable,
254  ATTR_MIN: self._minimum_minimum,
255  ATTR_MAX: self._maximum_maximum,
256  ATTR_PATTERN: self._config_config.get(CONF_PATTERN),
257  ATTR_MODE: self._config_config[CONF_MODE],
258  }
259 
260  async def async_added_to_hass(self) -> None:
261  """Run when entity about to be added to hass."""
262  await super().async_added_to_hass()
263  if self._current_value_current_value is not None:
264  return
265 
266  state = await self.async_get_last_stateasync_get_last_state()
267  value = state.state if state else None
268 
269  # Check against None because value can be 0
270  if value is not None and self._minimum_minimum <= len(value) <= self._maximum_maximum:
271  self._current_value_current_value = value
272 
273  async def async_set_value(self, value: str) -> None:
274  """Select new value."""
275  if len(value) < self._minimum_minimum or len(value) > self._maximum_maximum:
276  _LOGGER.warning(
277  "Invalid value: %s (length range %s - %s)",
278  value,
279  self._minimum_minimum,
280  self._maximum_maximum,
281  )
282  return
283  self._current_value_current_value = value
284  self.async_write_ha_stateasync_write_ha_state()
285 
286  async def async_update_config(self, config: ConfigType) -> None:
287  """Handle when the config is updated."""
288  self._config_config = config
289  self.async_write_ha_stateasync_write_ha_state()
dict[str, Any] _process_create_data(self, dict[str, Any] data)
Definition: __init__.py:166
dict[str, Any] _update_data(self, dict[str, Any] item, dict[str, Any] update_data)
Definition: __init__.py:177
Self from_yaml(cls, ConfigType config)
Definition: __init__.py:207
Self from_storage(cls, ConfigType config)
Definition: __init__.py:200
None __init__(self, ConfigType config)
Definition: __init__.py:194
None async_update_config(self, ConfigType config)
Definition: __init__.py:286
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:107
dict[str, Any] _cv_input_text(dict[str, Any] config)
Definition: __init__.py:65
None async_register_admin_service(HomeAssistant hass, str domain, str service, Callable[[ServiceCall], Awaitable[None]|None] service_func, VolSchemaType schema=vol.Schema({}, extra=vol.PREVENT_EXTRA))
Definition: service.py:1121