1 """Component to interface with various sensors that can be monitored."""
3 from __future__
import annotations
6 from collections.abc
import Mapping
7 from contextlib
import suppress
8 from dataclasses
import dataclass
9 from datetime
import UTC, date, datetime, timedelta
10 from decimal
import Decimal, InvalidOperation
as DecimalInvalidOperation
11 from functools
import partial
13 from math
import ceil, floor, isfinite, log10
14 from typing
import Any, Final, Self, cast, final, override
16 from propcache
import cached_property
20 _DEPRECATED_DEVICE_CLASS_AQI,
21 _DEPRECATED_DEVICE_CLASS_BATTERY,
22 _DEPRECATED_DEVICE_CLASS_CO,
23 _DEPRECATED_DEVICE_CLASS_CO2,
24 _DEPRECATED_DEVICE_CLASS_CURRENT,
25 _DEPRECATED_DEVICE_CLASS_DATE,
26 _DEPRECATED_DEVICE_CLASS_ENERGY,
27 _DEPRECATED_DEVICE_CLASS_FREQUENCY,
28 _DEPRECATED_DEVICE_CLASS_GAS,
29 _DEPRECATED_DEVICE_CLASS_HUMIDITY,
30 _DEPRECATED_DEVICE_CLASS_ILLUMINANCE,
31 _DEPRECATED_DEVICE_CLASS_MONETARY,
32 _DEPRECATED_DEVICE_CLASS_NITROGEN_DIOXIDE,
33 _DEPRECATED_DEVICE_CLASS_NITROGEN_MONOXIDE,
34 _DEPRECATED_DEVICE_CLASS_NITROUS_OXIDE,
35 _DEPRECATED_DEVICE_CLASS_OZONE,
36 _DEPRECATED_DEVICE_CLASS_PM1,
37 _DEPRECATED_DEVICE_CLASS_PM10,
38 _DEPRECATED_DEVICE_CLASS_PM25,
39 _DEPRECATED_DEVICE_CLASS_POWER,
40 _DEPRECATED_DEVICE_CLASS_POWER_FACTOR,
41 _DEPRECATED_DEVICE_CLASS_PRESSURE,
42 _DEPRECATED_DEVICE_CLASS_SIGNAL_STRENGTH,
43 _DEPRECATED_DEVICE_CLASS_SULPHUR_DIOXIDE,
44 _DEPRECATED_DEVICE_CLASS_TEMPERATURE,
45 _DEPRECATED_DEVICE_CLASS_TIMESTAMP,
46 _DEPRECATED_DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
47 _DEPRECATED_DEVICE_CLASS_VOLTAGE,
48 ATTR_UNIT_OF_MEASUREMENT,
49 CONF_UNIT_OF_MEASUREMENT,
57 all_with_deprecated_constants,
58 check_if_deprecated_constant,
59 dir_with_deprecated_constants,
71 _DEPRECATED_STATE_CLASS_MEASUREMENT,
72 _DEPRECATED_STATE_CLASS_TOTAL,
73 _DEPRECATED_STATE_CLASS_TOTAL_INCREASING,
78 DEVICE_CLASS_STATE_CLASSES,
81 DEVICE_CLASSES_SCHEMA,
83 NON_NUMERIC_DEVICE_CLASSES,
90 from .websocket_api
import async_setup
as async_setup_ws_api
92 _LOGGER: Final = logging.getLogger(__name__)
94 DATA_COMPONENT: HassKey[EntityComponent[SensorEntity]] = HassKey(DOMAIN)
95 ENTITY_ID_FORMAT: Final = DOMAIN +
".{}"
96 PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
97 PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
105 "DEVICE_CLASS_STATE_CLASSES",
107 "PLATFORM_SCHEMA_BASE",
112 "SensorEntityDescription",
113 "SensorExtraStoredData",
120 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
121 """Track states and offer events for sensors."""
122 component = hass.data[DATA_COMPONENT] = EntityComponent[SensorEntity](
123 _LOGGER, DOMAIN, hass, SCAN_INTERVAL
126 async_setup_ws_api(hass)
127 await component.async_setup(config)
132 """Set up a config entry."""
137 """Unload a config entry."""
142 """A class that describes sensor entities."""
144 device_class: SensorDeviceClass |
None =
None
145 last_reset: datetime |
None =
None
146 native_unit_of_measurement: str |
None =
None
147 options: list[str] |
None =
None
148 state_class: SensorStateClass | str |
None =
None
149 suggested_display_precision: int |
None =
None
150 suggested_unit_of_measurement: str |
None =
None
151 unit_of_measurement:
None =
None
155 device_class: SensorDeviceClass |
None,
156 state_class: SensorStateClass | str |
None,
157 native_unit_of_measurement: str |
None,
158 suggested_display_precision: int |
None,
160 """Return true if the sensor must be numeric."""
163 if device_class
in NON_NUMERIC_DEVICE_CLASSES:
166 state_class
is not None
167 or native_unit_of_measurement
is not None
168 or suggested_display_precision
is not None
173 return device_class
is not None
176 CACHED_PROPERTIES_WITH_ATTR_ = {
179 "native_unit_of_measurement",
183 "suggested_display_precision",
184 "suggested_unit_of_measurement",
187 TEMPERATURE_UNITS = {UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT}
191 """Base class for sensor entities."""
193 _entity_component_unrecorded_attributes = frozenset({ATTR_OPTIONS})
195 entity_description: SensorEntityDescription
196 _attr_device_class: SensorDeviceClass |
None
197 _attr_last_reset: datetime |
None
198 _attr_native_unit_of_measurement: str |
None
199 _attr_native_value: StateType | date | datetime | Decimal =
None
200 _attr_options: list[str] |
None
201 _attr_state_class: SensorStateClass | str |
None
202 _attr_state:
None =
None
203 _attr_suggested_display_precision: int |
None
204 _attr_suggested_unit_of_measurement: str |
None
205 _attr_unit_of_measurement:
None = (
208 _invalid_state_class_reported =
False
209 _invalid_unit_of_measurement_reported =
False
210 _last_reset_reported =
False
211 _sensor_option_display_precision: int |
None =
None
212 _sensor_option_unit_of_measurement: str |
None | UndefinedType = UNDEFINED
213 _invalid_suggested_unit_of_measurement_reported =
False
219 platform: EntityPlatform,
220 parallel_updates: asyncio.Semaphore |
None,
222 """Start adding an entity to a platform.
224 Allows integrations to remove legacy custom unit conversion which is no longer
225 needed without breaking existing sensors. Only works for sensors which are in
228 This can be removed once core integrations have dropped unneeded custom unit
236 registry = er.async_get(self.
hasshass)
240 entity_id := registry.async_get_entity_id(
241 platform.domain, platform.platform_name, self.
unique_idunique_id
249 registry_entry = registry.async_get(entity_id)
250 assert registry_entry
261 registry_unit = registry_entry.unit_of_measurement
264 (sensor_options := registry_entry.options.get(DOMAIN))
265 and CONF_UNIT_OF_MEASUREMENT
in sensor_options
267 or f
"{DOMAIN}.private" in registry_entry.options
275 or registry_unit
not in unit_converter.VALID_UNITS
285 {
"suggested_unit_of_measurement": registry_unit},
292 """Call when the sensor entity is added to hass."""
295 raise HomeAssistantError(
296 f
"Entity {self.entity_id} cannot be added as the entity category is set to config"
305 """Return True if an unnamed entity should be named by its device class.
307 For sensors this is True if the entity has a device class.
314 """Return the class of this entity."""
315 if hasattr(self,
"_attr_device_class"):
316 return self._attr_device_class
317 if hasattr(self,
"entity_description"):
318 return self.entity_description.device_class
324 """Return true if the sensor must be numeric."""
334 """Return a set of possible options."""
335 if hasattr(self,
"_attr_options"):
336 return self._attr_options
337 if hasattr(self,
"entity_description"):
338 return self.entity_description.options
343 """Return the state class of this entity, if any."""
344 if hasattr(self,
"_attr_state_class"):
345 return self._attr_state_class
346 if hasattr(self,
"entity_description"):
347 return self.entity_description.state_class
352 """Return the time when the sensor was last reset, if any."""
353 if hasattr(self,
"_attr_last_reset"):
354 return self._attr_last_reset
355 if hasattr(self,
"entity_description"):
356 return self.entity_description.last_reset
362 """Return the capability attributes."""
364 return {ATTR_STATE_CLASS: state_class}
366 if options := self.
optionsoptions:
367 return {ATTR_OPTIONS: options}
372 """Validate the suggested unit.
374 Validate that a unit converter exists for the sensor's device class and that the
375 unit converter supports both the native and the suggested units of measurement.
381 or suggested_unit_of_measurement
not in unit_converter.VALID_UNITS
386 f
"Entity {type(self)} suggest an incorrect "
387 f
"unit of measurement: {suggested_unit_of_measurement}."
394 """Return the initial unit."""
398 if suggested_unit_of_measurement
is None:
400 suggested_unit_of_measurement = self.
hasshass.config.units.get_converted_unit(
404 if suggested_unit_of_measurement
is None and (
409 suggested_unit_of_measurement = self.
hasshass.config.units.get_converted_unit(
413 if suggested_unit_of_measurement
is None:
420 return suggested_unit_of_measurement
423 """Return initial entity options.
425 These will be stored in the entity registry the first time the entity is seen,
426 and then only updated if the unit system is changed.
430 if suggested_unit_of_measurement
is UNDEFINED:
434 f
"{DOMAIN}.private": {
435 "suggested_unit_of_measurement": suggested_unit_of_measurement
443 """Return state attributes."""
446 if state_class != SensorStateClass.TOTAL:
448 f
"Entity {self.entity_id} ({type(self)}) with state_class {state_class}"
449 " has set last_reset. Setting last_reset for entities with state_class"
450 " other than 'total' is not supported. Please update your configuration"
451 " if state_class is manually configured."
454 if state_class == SensorStateClass.TOTAL:
455 return {ATTR_LAST_RESET: last_reset.isoformat()}
461 """Return the value reported by the sensor."""
462 return self._attr_native_value
466 """Return the suggested number of decimal digits for display."""
467 if hasattr(self,
"_attr_suggested_display_precision"):
468 return self._attr_suggested_display_precision
469 if hasattr(self,
"entity_description"):
470 return self.entity_description.suggested_display_precision
475 """Return the unit of measurement of the sensor, if any."""
476 if hasattr(self,
"_attr_native_unit_of_measurement"):
477 return self._attr_native_unit_of_measurement
478 if hasattr(self,
"entity_description"):
479 return self.entity_description.native_unit_of_measurement
484 """Return the unit which should be used for the sensor's state.
486 This can be used by integrations to override automatic unit conversion rules,
487 for example to make a temperature sensor display in °C even if the configured
488 unit system prefers °F.
490 For sensors without a `unique_id`, this takes precedence over legacy
491 temperature conversion rules only.
493 For sensors with a `unique_id`, this is applied only if the unit is not set by
494 the user, and takes precedence over automatic device-class conversion rules.
497 suggested_unit_of_measurement is stored in the entity registry the first
498 time the entity is seen, and then never updated.
501 if hasattr(self,
"_attr_suggested_unit_of_measurement"):
502 return self._attr_suggested_unit_of_measurement
503 if hasattr(self,
"entity_description"):
504 return self.entity_description.suggested_unit_of_measurement
511 """Return the unit of measurement of the entity, after unit conversion."""
524 return suggested_unit_of_measurement
529 native_unit_of_measurement
in TEMPERATURE_UNITS
532 return self.
hasshass.config.units.temperature_unit
537 := self.
platformplatform.default_language_platform_translations.get(translation_key)
539 if native_unit_of_measurement
is not None:
541 f
"Sensor {type(self)} from integration '{self.platform.platform_name}' "
542 f
"has a translation key for unit_of_measurement '{unit_of_measurement}', "
543 f
"but also has a native_unit_of_measurement '{native_unit_of_measurement}'"
545 return unit_of_measurement
548 return native_unit_of_measurement
554 """Return the state of the sensor and perform unit conversions, if needed."""
565 if device_class
in NON_NUMERIC_DEVICE_CLASSES
and unit_of_measurement:
567 f
"Sensor {self.entity_id} has a unit of measurement and thus "
568 "indicating it has a numeric value; however, it has the "
569 f
"non-numeric device class: {device_class}"
577 and (classes := DEVICE_CLASS_STATE_CLASSES.get(device_class))
is not None
578 and state_class
not in classes
585 "Entity %s (%s) is using state class '%s' which "
586 "is impossible considering device class ('%s') it is using; "
588 "Please update your configuration if your entity is manually "
589 "configured, otherwise %s",
594 "None or one of " if classes
else "None",
595 ", ".join(f
"'{value.value}'" for value
in classes),
604 if device_class
is SensorDeviceClass.TIMESTAMP:
608 value = cast(datetime, value)
609 if value.tzinfo
is None:
611 f
"Invalid datetime: {self.entity_id} provides state '{value}', "
612 "which is missing timezone information"
615 if value.tzinfo != UTC:
616 value = value.astimezone(UTC)
618 return value.isoformat(timespec=
"seconds")
619 except (AttributeError, OverflowError, TypeError)
as err:
621 f
"Invalid datetime: {self.entity_id} has timestamp device class "
622 f
"but provides state {value}:{type(value)} resulting in '{err}'"
626 if device_class
is SensorDeviceClass.DATE:
630 value = cast(date, value)
631 return value.isoformat()
632 except (AttributeError, TypeError)
as err:
634 f
"Invalid date: {self.entity_id} has date device class "
635 f
"but provides state {value}:{type(value)} resulting in '{err}'"
641 )
is not None or device_class
is SensorDeviceClass.ENUM:
642 if device_class
is not SensorDeviceClass.ENUM:
643 reason =
"is missing the enum device class"
644 if device_class
is not None:
645 reason = f
"has device class '{device_class}' instead of 'enum'"
647 f
"Sensor {self.entity_id} is providing enum options, but {reason}"
650 if options
and value
not in options:
652 f
"Sensor {self.entity_id} provides state value '{value}', "
653 "which is not in the list of options provided"
662 device_class, state_class, native_unit_of_measurement, suggested_precision
667 numerical_value: int | float | Decimal
668 if not isinstance(value, (int, float, Decimal)):
670 if isinstance(value, str)
and "." not in value
and "e" not in value:
672 numerical_value =
int(value)
675 numerical_value =
float(value)
677 numerical_value =
float(value)
678 except (TypeError, ValueError)
as err:
680 f
"Sensor {self.entity_id} has device class '{device_class}', "
681 f
"state class '{state_class}' unit '{unit_of_measurement}' and "
682 f
"suggested precision '{suggested_precision}' thus indicating it "
683 f
"has a numeric value; however, it has the non-numeric value: "
684 f
"'{value}' ({type(value)})"
687 numerical_value = value
689 if not isfinite(numerical_value):
691 f
"Sensor {self.entity_id} has device class '{device_class}', "
692 f
"state class '{state_class}' unit '{unit_of_measurement}' and "
693 f
"suggested precision '{suggested_precision}' thus indicating it "
694 f
"has a numeric value; however, it has the non-finite value: "
695 f
"'{numerical_value}'"
698 if native_unit_of_measurement != unit_of_measurement
and (
699 converter := UNIT_CONVERTERS.get(device_class)
702 converted_numerical_value = converter.converter_factory(
703 native_unit_of_measurement,
705 )(
float(numerical_value))
710 suggested_precision
is None
716 len(value_s) - value_s.index(
".") - 1
if "." in value_s
else 0
724 converter.get_unit_ratio(
725 native_unit_of_measurement, unit_of_measurement
729 precision = precision + floor(ratio_log)
731 value = f
"{converted_numerical_value:z.{precision}f}"
733 value = converted_numerical_value
739 and (units := DEVICE_CLASS_UNITS.get(device_class))
is not None
740 and native_unit_of_measurement
not in units
748 "Entity %s (%s) is using native unit of measurement '%s' which "
749 "is not a valid unit for the device class ('%s') it is using; "
750 "expected one of %s; "
751 "Please update your configuration if your entity is manually "
752 "configured, otherwise %s"
756 native_unit_of_measurement,
758 [
str(unit)
if unit
else "no unit of measurement" for unit
in units],
765 """Return display precision, or None if not set."""
770 for option
in (
"display_precision",
"suggested_display_precision"):
771 if (precision := sensor_options.get(option))
is not None:
772 return cast(int, precision)
776 """Update suggested display precision stored in registry."""
781 default_unit_of_measurement = (
787 display_precision
is not None
788 and default_unit_of_measurement != unit_of_measurement
789 and device_class
in UNIT_CONVERTERS
791 converter = UNIT_CONVERTERS[device_class]
796 converter.get_unit_ratio(
797 default_unit_of_measurement, unit_of_measurement
800 ratio_log = floor(ratio_log)
if ratio_log > 0
else ceil(ratio_log)
801 display_precision =
max(0, display_precision + ratio_log)
804 if "suggested_display_precision" not in sensor_options:
805 if display_precision
is None:
807 elif sensor_options[
"suggested_display_precision"] == display_precision:
810 registry = er.async_get(self.
hasshass)
811 sensor_options =
dict(sensor_options)
812 sensor_options.pop(
"suggested_display_precision",
None)
813 if display_precision
is not None:
814 sensor_options[
"suggested_display_precision"] = display_precision
815 registry.async_update_entity_options(
816 self.
entity_identity_id, DOMAIN, sensor_options
or None
820 self, primary_key: str, secondary_key: str
821 ) -> str |
None | UndefinedType:
822 """Return a custom unit, or UNDEFINED if not compatible with the native unit."""
826 and secondary_key
in sensor_options
829 in UNIT_CONVERTERS[device_class].VALID_UNITS
830 and (custom_unit := sensor_options[secondary_key])
831 in UNIT_CONVERTERS[device_class].VALID_UNITS
833 return cast(str, custom_unit)
838 """Run when the entity registry entry has been updated."""
844 """Read entity options from entity registry.
846 Called when the entity registry entry has been updated and before the sensor is
847 added to the state machine.
853 )
and "refresh_initial_entity_options" in sensor_options:
854 registry = er.async_get(self.
hasshass)
856 registry.async_update_entity_options(
859 initial_options.get(f
"{DOMAIN}.private"),
862 DOMAIN, CONF_UNIT_OF_MEASUREMENT
866 f
"{DOMAIN}.private",
"suggested_unit_of_measurement"
872 """Object to hold extra stored data."""
874 native_value: StateType | date | datetime | Decimal
875 native_unit_of_measurement: str |
None
878 """Return a dict representation of the sensor data."""
879 native_value: StateType | date | datetime | Decimal | dict[str, str] = (
882 if isinstance(native_value, (date, datetime)):
884 "__type":
str(type(native_value)),
885 "isoformat": native_value.isoformat(),
887 if isinstance(native_value, Decimal):
889 "__type":
str(type(native_value)),
890 "decimal_str":
str(native_value),
893 "native_value": native_value,
894 "native_unit_of_measurement": self.native_unit_of_measurement,
898 def from_dict(cls, restored: dict[str, Any]) -> Self |
None:
899 """Initialize a stored sensor state from a dict."""
901 native_value = restored[
"native_value"]
902 native_unit_of_measurement = restored[
"native_unit_of_measurement"]
906 type_ = native_value[
"__type"]
907 if type_ ==
"<class 'datetime.datetime'>":
908 native_value = dt_util.parse_datetime(native_value[
"isoformat"])
909 elif type_ ==
"<class 'datetime.date'>":
910 native_value = dt_util.parse_date(native_value[
"isoformat"])
911 elif type_ ==
"<class 'decimal.Decimal'>":
912 native_value = Decimal(native_value[
"decimal_str"])
919 except DecimalInvalidOperation:
923 return cls(native_value, native_unit_of_measurement)
927 """Mixin class for restoring previous sensor state."""
931 """Return sensor specific state data to be restored."""
935 """Restore native_value and native_unit_of_measurement."""
936 if (restored_last_extra_data := await self.async_get_last_extra_data())
is None:
938 return SensorExtraStoredData.from_dict(restored_last_extra_data.as_dict())
943 """Update the suggested_unit_of_measurement according to the unit system."""
944 registry = er.async_get(hass)
946 for entry
in registry.entities.values():
947 if entry.domain != DOMAIN:
950 sensor_private_options =
dict(entry.options.get(f
"{DOMAIN}.private", {}))
951 sensor_private_options[
"refresh_initial_entity_options"] =
True
952 registry.async_update_entity_options(
955 sensor_private_options,
960 """Return the display precision."""
961 if not (entry := er.async_get(hass).
async_get(entity_id))
or not (
962 sensor_options := entry.options.get(DOMAIN)
965 if (display_precision := sensor_options.get(
"display_precision"))
is not None:
966 return cast(int, display_precision)
967 return sensor_options.get(
"suggested_display_precision")
972 """Return the state rounded for presentation."""
977 with suppress(TypeError, ValueError):
978 numerical_value =
float(value)
979 value = f
"{numerical_value:z.{precision}f}"
987 __getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
989 dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
SensorExtraStoredData extra_restore_state_data(self)
SensorExtraStoredData|None async_get_last_sensor_data(self)
datetime|None last_reset(self)
SensorDeviceClass|None device_class(self)
_sensor_option_unit_of_measurement
dict[str, Any]|None capability_attributes(self)
bool _default_to_device_class_name(self)
None _update_suggested_precision(self)
bool _numeric_state_expected(self)
None async_registry_entry_updated(self)
None _async_read_entity_options(self)
list[str]|None options(self)
dict[str, Any]|None state_attributes(self)
_invalid_unit_of_measurement_reported
SensorStateClass|str|None state_class(self)
int|None _display_precision_or_none(self)
StateType|date|datetime|Decimal native_value(self)
str|None native_unit_of_measurement(self)
None async_internal_added_to_hass(self)
er.EntityOptionsType|None get_initial_entity_options(self)
_sensor_option_display_precision
str|None|UndefinedType _custom_unit_or_undef(self, str primary_key, str secondary_key)
_invalid_state_class_reported
_invalid_suggested_unit_of_measurement_reported
str|None suggested_unit_of_measurement(self)
str|None unit_of_measurement(self)
None add_to_platform_start(self, HomeAssistant hass, EntityPlatform platform, asyncio.Semaphore|None parallel_updates)
bool _is_valid_suggested_unit(self, str suggested_unit_of_measurement)
int|None suggested_display_precision(self)
str|UndefinedType _get_initial_suggested_unit(self)
str _suggest_report_issue(self)
str|None device_class(self)
EntityCategory|None entity_category(self)
er.EntityOptionsType|None get_initial_entity_options(self)
str|None _unit_of_measurement_translation_key(self)
str|None unit_of_measurement(self)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
None async_update_suggested_units(HomeAssistant hass)
bool async_setup(HomeAssistant hass, ConfigType config)
str async_rounded_state(HomeAssistant hass, str entity_id, State state)
int|None _display_precision(HomeAssistant hass, str entity_id)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
bool _numeric_state_expected(SensorDeviceClass|None device_class, SensorStateClass|str|None state_class, str|None native_unit_of_measurement, int|None suggested_display_precision)
AreaRegistry async_get(HomeAssistant hass)
list[str] all_with_deprecated_constants(dict[str, Any] module_globals)