Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Component to allow numeric input for platforms."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from contextlib import suppress
7 import dataclasses
8 from datetime import timedelta
9 import logging
10 from math import ceil, floor
11 from typing import TYPE_CHECKING, Any, Self, final
12 
13 from propcache import cached_property
14 import voluptuous as vol
15 
16 from homeassistant.config_entries import ConfigEntry
17 from homeassistant.const import ATTR_MODE, CONF_UNIT_OF_MEASUREMENT, UnitOfTemperature
18 from homeassistant.core import (
19  HomeAssistant,
20  ServiceCall,
21  async_get_hass_or_none,
22  callback,
23 )
24 from homeassistant.exceptions import ServiceValidationError
25 from homeassistant.helpers import config_validation as cv
26 from homeassistant.helpers.entity import Entity, EntityDescription
27 from homeassistant.helpers.entity_component import EntityComponent
28 from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
29 from homeassistant.helpers.typing import ConfigType
30 from homeassistant.loader import async_suggest_report_issue
31 from homeassistant.util.hass_dict import HassKey
32 
33 from .const import ( # noqa: F401
34  ATTR_MAX,
35  ATTR_MIN,
36  ATTR_STEP,
37  ATTR_STEP_VALIDATION,
38  ATTR_VALUE,
39  DEFAULT_MAX_VALUE,
40  DEFAULT_MIN_VALUE,
41  DEFAULT_STEP,
42  DEVICE_CLASSES_SCHEMA,
43  DOMAIN,
44  SERVICE_SET_VALUE,
45  UNIT_CONVERTERS,
46  NumberDeviceClass,
47  NumberMode,
48 )
49 from .websocket_api import async_setup as async_setup_ws_api
50 
51 _LOGGER = logging.getLogger(__name__)
52 
53 DATA_COMPONENT: HassKey[EntityComponent[NumberEntity]] = HassKey(DOMAIN)
54 ENTITY_ID_FORMAT = DOMAIN + ".{}"
55 PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
56 PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
57 SCAN_INTERVAL = timedelta(seconds=30)
58 
59 MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
60 
61 
62 __all__ = [
63  "ATTR_MAX",
64  "ATTR_MIN",
65  "ATTR_STEP",
66  "ATTR_VALUE",
67  "DEFAULT_MAX_VALUE",
68  "DEFAULT_MIN_VALUE",
69  "DEFAULT_STEP",
70  "DOMAIN",
71  "PLATFORM_SCHEMA_BASE",
72  "PLATFORM_SCHEMA",
73  "NumberDeviceClass",
74  "NumberEntity",
75  "NumberEntityDescription",
76  "NumberExtraStoredData",
77  "NumberMode",
78  "RestoreNumber",
79 ]
80 
81 # mypy: disallow-any-generics
82 
83 
84 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
85  """Set up Number entities."""
86  component = hass.data[DATA_COMPONENT] = EntityComponent[NumberEntity](
87  _LOGGER, DOMAIN, hass, SCAN_INTERVAL
88  )
89  async_setup_ws_api(hass)
90  await component.async_setup(config)
91 
92  component.async_register_entity_service(
93  SERVICE_SET_VALUE,
94  {vol.Required(ATTR_VALUE): vol.Coerce(float)},
95  async_set_value,
96  )
97 
98  return True
99 
100 
101 async def async_set_value(entity: NumberEntity, service_call: ServiceCall) -> None:
102  """Service call wrapper to set a new value."""
103  value = service_call.data["value"]
104  if value < entity.min_value or value > entity.max_value:
106  translation_domain=DOMAIN,
107  translation_key="out_of_range",
108  translation_placeholders={
109  "value": value,
110  "entity_id": entity.entity_id,
111  "min_value": str(entity.min_value),
112  "max_value": str(entity.max_value),
113  },
114  )
115 
116  try:
117  native_value = entity.convert_to_native_value(value)
118  # Clamp to the native range
119  native_value = min(
120  max(native_value, entity.native_min_value), entity.native_max_value
121  )
122  await entity.async_set_native_value(native_value)
123  except NotImplementedError:
124  await entity.async_set_value(value)
125 
126 
127 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
128  """Set up a config entry."""
129  return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
130 
131 
132 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
133  """Unload a config entry."""
134  return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
135 
136 
137 class NumberEntityDescription(EntityDescription, frozen_or_thawed=True):
138  """A class that describes number entities."""
139 
140  device_class: NumberDeviceClass | None = None
141  max_value: None = None
142  min_value: None = None
143  mode: NumberMode | None = None
144  native_max_value: float | None = None
145  native_min_value: float | None = None
146  native_step: float | None = None
147  native_unit_of_measurement: str | None = None
148  step: None = None
149  unit_of_measurement: None = None # Type override, use native_unit_of_measurement
150 
151 
152 def ceil_decimal(value: float, precision: float = 0) -> float:
153  """Return the ceiling of f with d decimals.
154 
155  This is a simple implementation which ignores floating point inexactness.
156  """
157  factor = 10**precision
158  return ceil(value * factor) / factor
159 
160 
161 def floor_decimal(value: float, precision: float = 0) -> float:
162  """Return the floor of f with d decimals.
163 
164  This is a simple implementation which ignores floating point inexactness.
165  """
166  factor = 10**precision
167  return floor(value * factor) / factor
168 
169 
170 CACHED_PROPERTIES_WITH_ATTR_ = {
171  "device_class",
172  "native_max_value",
173  "native_min_value",
174  "native_step",
175  "mode",
176  "native_unit_of_measurement",
177  "native_value",
178 }
179 
180 
181 class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
182  """Representation of a Number entity."""
183 
184  _entity_component_unrecorded_attributes = frozenset(
185  {ATTR_MIN, ATTR_MAX, ATTR_STEP, ATTR_STEP_VALIDATION, ATTR_MODE}
186  )
187 
188  entity_description: NumberEntityDescription
189  _attr_device_class: NumberDeviceClass | None
190  _attr_max_value: None
191  _attr_min_value: None
192  _attr_mode: NumberMode
193  _attr_state: None = None
194  _attr_step: None
195  _attr_unit_of_measurement: None # Subclasses of NumberEntity should not set this
196  _attr_value: None
197  _attr_native_max_value: float
198  _attr_native_min_value: float
199  _attr_native_step: float
200  _attr_native_unit_of_measurement: str | None
201  _attr_native_value: float | None = None
202  _deprecated_number_entity_reported = False
203  _number_option_unit_of_measurement: str | None = None
204 
205  def __init_subclass__(cls, **kwargs: Any) -> None:
206  """Post initialisation processing."""
207  super().__init_subclass__(**kwargs)
208  if any(
209  method in cls.__dict__
210  for method in (
211  "async_set_value",
212  "max_value",
213  "min_value",
214  "set_value",
215  "step",
216  "unit_of_measurement",
217  "value",
218  )
219  ):
220  report_issue = async_suggest_report_issue(
221  async_get_hass_or_none(), module=cls.__module__
222  )
223  _LOGGER.warning(
224  (
225  "%s::%s is overriding deprecated methods on an instance of "
226  "NumberEntity, this is not valid and will be unsupported "
227  "from Home Assistant 2022.10. Please %s"
228  ),
229  cls.__module__,
230  cls.__name__,
231  report_issue,
232  )
233 
234  async def async_internal_added_to_hass(self) -> None:
235  """Call when the number entity is added to hass."""
236  await super().async_internal_added_to_hass()
237  if not self.registry_entryregistry_entry:
238  return
239  self.async_registry_entry_updatedasync_registry_entry_updatedasync_registry_entry_updated()
240 
241  @property
242  def capability_attributes(self) -> dict[str, Any]:
243  """Return capability attributes."""
244  device_class = self.device_classdevice_classdevice_classdevice_class
245  min_value = self._convert_to_state_value_convert_to_state_value(
246  self.native_min_valuenative_min_value, floor_decimal, device_class
247  )
248  max_value = self._convert_to_state_value_convert_to_state_value(
249  self.native_max_valuenative_max_value, ceil_decimal, device_class
250  )
251  return {
252  ATTR_MIN: min_value,
253  ATTR_MAX: max_value,
254  ATTR_STEP: self._calculate_step_calculate_step(min_value, max_value),
255  ATTR_MODE: self.modemode,
256  }
257 
258  def _default_to_device_class_name(self) -> bool:
259  """Return True if an unnamed entity should be named by its device class.
260 
261  For numbers this is True if the entity has a device class.
262  """
263  return self.device_classdevice_classdevice_classdevice_class is not None
264 
265  @cached_property
266  def device_class(self) -> NumberDeviceClass | None:
267  """Return the class of this entity."""
268  if hasattr(self, "_attr_device_class"):
269  return self._attr_device_class
270  if hasattr(self, "entity_description"):
271  return self.entity_description.device_class
272  return None
273 
274  @cached_property
275  def native_min_value(self) -> float:
276  """Return the minimum value."""
277  if hasattr(self, "_attr_native_min_value"):
278  return self._attr_native_min_value
279  if (
280  hasattr(self, "entity_description")
281  and self.entity_description.native_min_value is not None
282  ):
283  return self.entity_description.native_min_value
284  return DEFAULT_MIN_VALUE
285 
286  @property
287  @final
288  def min_value(self) -> float:
289  """Return the minimum value."""
290  return self._convert_to_state_value_convert_to_state_value(
291  self.native_min_valuenative_min_value, floor_decimal, self.device_classdevice_classdevice_classdevice_class
292  )
293 
294  @cached_property
295  def native_max_value(self) -> float:
296  """Return the maximum value."""
297  if hasattr(self, "_attr_native_max_value"):
298  return self._attr_native_max_value
299  if (
300  hasattr(self, "entity_description")
301  and self.entity_description.native_max_value is not None
302  ):
303  return self.entity_description.native_max_value
304  return DEFAULT_MAX_VALUE
305 
306  @property
307  @final
308  def max_value(self) -> float:
309  """Return the maximum value."""
310  return self._convert_to_state_value_convert_to_state_value(
311  self.native_max_valuenative_max_value, ceil_decimal, self.device_classdevice_classdevice_classdevice_class
312  )
313 
314  @cached_property
315  def native_step(self) -> float | None:
316  """Return the increment/decrement step."""
317  if hasattr(self, "_attr_native_step"):
318  return self._attr_native_step
319  if (
320  hasattr(self, "entity_description")
321  and self.entity_description.native_step is not None
322  ):
323  return self.entity_description.native_step
324  return None
325 
326  @property
327  @final
328  def step(self) -> float:
329  """Return the increment/decrement step."""
330  return self._calculate_step_calculate_step(self.min_valuemin_value, self.max_valuemax_value)
331 
332  def _calculate_step(self, min_value: float, max_value: float) -> float:
333  """Return the increment/decrement step."""
334  if (native_step := self.native_stepnative_step) is not None:
335  return native_step
336  step = DEFAULT_STEP
337  value_range = abs(max_value - min_value)
338  if value_range != 0:
339  while value_range <= step:
340  step /= 10.0
341  return step
342 
343  @cached_property
344  def mode(self) -> NumberMode:
345  """Return the mode of the entity."""
346  if hasattr(self, "_attr_mode"):
347  return self._attr_mode
348  if (
349  hasattr(self, "entity_description")
350  and self.entity_description.mode is not None
351  ):
352  return self.entity_description.mode
353  return NumberMode.AUTO
354 
355  @property
356  @final
357  def state(self) -> float | None:
358  """Return the entity state."""
359  return self.valuevalue
360 
361  @cached_property
362  def native_unit_of_measurement(self) -> str | None:
363  """Return the unit of measurement of the entity, if any."""
364  if hasattr(self, "_attr_native_unit_of_measurement"):
365  return self._attr_native_unit_of_measurement
366  if hasattr(self, "entity_description"):
367  return self.entity_description.native_unit_of_measurement
368  return None
369 
370  @property
371  @final
372  def unit_of_measurement(self) -> str | None:
373  """Return the unit of measurement of the entity, after unit conversion."""
374  if self._number_option_unit_of_measurement_number_option_unit_of_measurement:
375  return self._number_option_unit_of_measurement_number_option_unit_of_measurement
376 
377  native_unit_of_measurement = self.native_unit_of_measurementnative_unit_of_measurement
378  # device_class is checked after native_unit_of_measurement since most
379  # of the time we can avoid the device_class check
380  if (
381  native_unit_of_measurement
382  in (UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT)
383  and self.device_classdevice_classdevice_classdevice_class == NumberDeviceClass.TEMPERATURE
384  ):
385  return self.hasshass.config.units.temperature_unit
386 
387  if (translation_key := self._unit_of_measurement_translation_key_unit_of_measurement_translation_key) and (
388  unit_of_measurement
389  := self.platformplatform.default_language_platform_translations.get(translation_key)
390  ):
391  if native_unit_of_measurement is not None:
392  raise ValueError(
393  f"Number entity {type(self)} from integration '{self.platform.platform_name}' "
394  f"has a translation key for unit_of_measurement '{unit_of_measurement}', "
395  f"but also has a native_unit_of_measurement '{native_unit_of_measurement}'"
396  )
397  return unit_of_measurement
398 
399  return native_unit_of_measurement
400 
401  @cached_property
402  def native_value(self) -> float | None:
403  """Return the value reported by the number."""
404  return self._attr_native_value
405 
406  @property
407  @final
408  def value(self) -> float | None:
409  """Return the entity value to represent the entity state."""
410  if (native_value := self.native_valuenative_value) is None:
411  return native_value
412  return self._convert_to_state_value_convert_to_state_value(native_value, round, self.device_classdevice_classdevice_classdevice_class)
413 
414  def set_native_value(self, value: float) -> None:
415  """Set new value."""
416  raise NotImplementedError
417 
418  async def async_set_native_value(self, value: float) -> None:
419  """Set new value."""
420  await self.hasshass.async_add_executor_job(self.set_native_valueset_native_value, value)
421 
422  @final
423  def set_value(self, value: float) -> None:
424  """Set new value."""
425  raise NotImplementedError
426 
427  @final
428  async def async_set_value(self, value: float) -> None:
429  """Set new value."""
430  await self.hasshass.async_add_executor_job(self.set_valueset_value, value)
431 
433  self,
434  value: float,
435  method: Callable[[float, int], float],
436  device_class: NumberDeviceClass | None,
437  ) -> float:
438  """Convert a value in the number's native unit to the configured unit."""
439  # device_class is checked first since most of the time we can avoid
440  # the unit conversion
441  if device_class not in UNIT_CONVERTERS:
442  return value
443 
444  native_unit_of_measurement = self.native_unit_of_measurementnative_unit_of_measurement
445  unit_of_measurement = self.unit_of_measurementunit_of_measurementunit_of_measurement
446  if native_unit_of_measurement != unit_of_measurement:
447  if TYPE_CHECKING:
448  assert native_unit_of_measurement
449  assert unit_of_measurement
450 
451  value_s = str(value)
452  prec = len(value_s) - value_s.index(".") - 1 if "." in value_s else 0
453 
454  # Suppress ValueError (Could not convert value to float)
455  with suppress(ValueError):
456  value_new: float = UNIT_CONVERTERS[device_class].converter_factory(
457  native_unit_of_measurement,
458  unit_of_measurement,
459  )(value)
460 
461  # Round to the wanted precision
462  return method(value_new, prec)
463 
464  return value
465 
466  def convert_to_native_value(self, value: float) -> float:
467  """Convert a value to the number's native unit."""
468  # device_class is checked first since most of the time we can avoid
469  # the unit conversion
470  if value is None or (device_class := self.device_classdevice_classdevice_classdevice_class) not in UNIT_CONVERTERS:
471  return value
472 
473  native_unit_of_measurement = self.native_unit_of_measurementnative_unit_of_measurement
474  unit_of_measurement = self.unit_of_measurementunit_of_measurementunit_of_measurement
475  if native_unit_of_measurement != unit_of_measurement:
476  if TYPE_CHECKING:
477  assert native_unit_of_measurement
478  assert unit_of_measurement
479 
480  return UNIT_CONVERTERS[device_class].converter_factory(
481  unit_of_measurement,
482  native_unit_of_measurement,
483  )(value)
484 
485  return value
486 
487  @callback
488  def async_registry_entry_updated(self) -> None:
489  """Run when the entity registry entry has been updated."""
490  if TYPE_CHECKING:
491  assert self.registry_entryregistry_entry
492  if (
493  (number_options := self.registry_entryregistry_entry.options.get(DOMAIN))
494  and (custom_unit := number_options.get(CONF_UNIT_OF_MEASUREMENT))
495  and (device_class := self.device_classdevice_classdevice_classdevice_class) in UNIT_CONVERTERS
496  and self.native_unit_of_measurementnative_unit_of_measurement
497  in UNIT_CONVERTERS[device_class].VALID_UNITS
498  and custom_unit in UNIT_CONVERTERS[device_class].VALID_UNITS
499  ):
500  self._number_option_unit_of_measurement_number_option_unit_of_measurement = custom_unit
501  return
502 
503  self._number_option_unit_of_measurement_number_option_unit_of_measurement = None
504 
505 
506 @dataclasses.dataclass
508  """Object to hold extra stored data."""
509 
510  native_max_value: float | None
511  native_min_value: float | None
512  native_step: float | None
513  native_unit_of_measurement: str | None
514  native_value: float | None
515 
516  def as_dict(self) -> dict[str, Any]:
517  """Return a dict representation of the number data."""
518  return dataclasses.asdict(self)
519 
520  @classmethod
521  def from_dict(cls, restored: dict[str, Any]) -> Self | None:
522  """Initialize a stored number state from a dict."""
523  try:
524  return cls(
525  restored["native_max_value"],
526  restored["native_min_value"],
527  restored["native_step"],
528  restored["native_unit_of_measurement"],
529  restored["native_value"],
530  )
531  except KeyError:
532  return None
533 
534 
536  """Mixin class for restoring previous number state."""
537 
538  @property
539  def extra_restore_state_data(self) -> NumberExtraStoredData:
540  """Return number specific state data to be restored."""
541  return NumberExtraStoredData(
542  self.native_max_valuenative_max_value,
543  self.native_min_valuenative_min_value,
544  self.native_stepnative_step,
545  self.native_unit_of_measurementnative_unit_of_measurement,
546  self.native_valuenative_value,
547  )
548 
549  async def async_get_last_number_data(self) -> NumberExtraStoredData | None:
550  """Restore native_*."""
551  if (restored_last_extra_data := await self.async_get_last_extra_dataasync_get_last_extra_data()) is None:
552  return None
553  return NumberExtraStoredData.from_dict(restored_last_extra_data.as_dict())
NumberDeviceClass|None device_class(self)
Definition: __init__.py:266
None async_set_value(self, float value)
Definition: __init__.py:428
dict[str, Any] capability_attributes(self)
Definition: __init__.py:242
None set_native_value(self, float value)
Definition: __init__.py:414
float convert_to_native_value(self, float value)
Definition: __init__.py:466
float _calculate_step(self, float min_value, float max_value)
Definition: __init__.py:332
None async_set_native_value(self, float value)
Definition: __init__.py:418
float _convert_to_state_value(self, float value, Callable[[float, int], float] method, NumberDeviceClass|None device_class)
Definition: __init__.py:437
None __init_subclass__(cls, **Any kwargs)
Definition: __init__.py:205
Self|None from_dict(cls, dict[str, Any] restored)
Definition: __init__.py:521
NumberExtraStoredData|None async_get_last_number_data(self)
Definition: __init__.py:549
NumberExtraStoredData extra_restore_state_data(self)
Definition: __init__.py:539
str|None _unit_of_measurement_translation_key(self)
Definition: entity.py:651
str|None unit_of_measurement(self)
Definition: entity.py:815
ExtraStoredData|None async_get_last_extra_data(self)
float floor_decimal(float value, float precision=0)
Definition: __init__.py:161
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:127
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:84
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:132
float ceil_decimal(float value, float precision=0)
Definition: __init__.py:152
None async_set_value(NumberEntity entity, ServiceCall service_call)
Definition: __init__.py:101
HomeAssistant|None async_get_hass_or_none()
Definition: core.py:299
str async_suggest_report_issue(HomeAssistant|None hass, *Integration|None integration=None, str|None integration_domain=None, str|None module=None)
Definition: loader.py:1752