Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for displaying minimal, maximal, mean or median values."""
2 
3 from __future__ import annotations
4 
5 from datetime import datetime
6 import logging
7 import statistics
8 from typing import Any
9 
10 import voluptuous as vol
11 
13  PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
14  SensorEntity,
15  SensorStateClass,
16 )
17 from homeassistant.config_entries import ConfigEntry
18 from homeassistant.const import (
19  ATTR_UNIT_OF_MEASUREMENT,
20  CONF_NAME,
21  CONF_TYPE,
22  CONF_UNIQUE_ID,
23  STATE_UNAVAILABLE,
24  STATE_UNKNOWN,
25 )
26 from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback
27 from homeassistant.helpers import config_validation as cv, entity_registry as er
28 from homeassistant.helpers.entity_platform import AddEntitiesCallback
29 from homeassistant.helpers.event import async_track_state_change_event
30 from homeassistant.helpers.reload import async_setup_reload_service
31 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
32 
33 from . import PLATFORMS
34 from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN
35 
36 _LOGGER = logging.getLogger(__name__)
37 
38 ATTR_MIN_VALUE = "min_value"
39 ATTR_MIN_ENTITY_ID = "min_entity_id"
40 ATTR_MAX_VALUE = "max_value"
41 ATTR_MAX_ENTITY_ID = "max_entity_id"
42 ATTR_MEAN = "mean"
43 ATTR_MEDIAN = "median"
44 ATTR_LAST = "last"
45 ATTR_LAST_ENTITY_ID = "last_entity_id"
46 ATTR_RANGE = "range"
47 ATTR_SUM = "sum"
48 
49 ICON = "mdi:calculator"
50 
51 SENSOR_TYPES = {
52  ATTR_MIN_VALUE: "min",
53  ATTR_MAX_VALUE: "max",
54  ATTR_MEAN: "mean",
55  ATTR_MEDIAN: "median",
56  ATTR_LAST: "last",
57  ATTR_RANGE: "range",
58  ATTR_SUM: "sum",
59 }
60 SENSOR_TYPE_TO_ATTR = {v: k for k, v in SENSOR_TYPES.items()}
61 
62 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
63  {
64  vol.Optional(CONF_TYPE, default=SENSOR_TYPES[ATTR_MAX_VALUE]): vol.All(
65  cv.string, vol.In(SENSOR_TYPES.values())
66  ),
67  vol.Optional(CONF_NAME): cv.string,
68  vol.Required(CONF_ENTITY_IDS): cv.entity_ids,
69  vol.Optional(CONF_ROUND_DIGITS, default=2): vol.Coerce(int),
70  vol.Optional(CONF_UNIQUE_ID): cv.string,
71  }
72 )
73 
74 
76  hass: HomeAssistant,
77  config_entry: ConfigEntry,
78  async_add_entities: AddEntitiesCallback,
79 ) -> None:
80  """Initialize min/max/mean config entry."""
81  registry = er.async_get(hass)
82  entity_ids = er.async_validate_entity_ids(
83  registry, config_entry.options[CONF_ENTITY_IDS]
84  )
85  sensor_type = config_entry.options[CONF_TYPE]
86  round_digits = int(config_entry.options[CONF_ROUND_DIGITS])
87 
89  [
91  entity_ids,
92  config_entry.title,
93  sensor_type,
94  round_digits,
95  config_entry.entry_id,
96  )
97  ]
98  )
99 
100 
102  hass: HomeAssistant,
103  config: ConfigType,
104  async_add_entities: AddEntitiesCallback,
105  discovery_info: DiscoveryInfoType | None = None,
106 ) -> None:
107  """Set up the min/max/mean sensor."""
108  entity_ids: list[str] = config[CONF_ENTITY_IDS]
109  name: str | None = config.get(CONF_NAME)
110  sensor_type: str = config[CONF_TYPE]
111  round_digits: int = config[CONF_ROUND_DIGITS]
112  unique_id = config.get(CONF_UNIQUE_ID)
113 
114  await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
115 
117  [MinMaxSensor(entity_ids, name, sensor_type, round_digits, unique_id)]
118  )
119 
120 
121 def calc_min(sensor_values: list[tuple[str, Any]]) -> tuple[str | None, float | None]:
122  """Calculate min value, honoring unknown states."""
123  val: float | None = None
124  entity_id: str | None = None
125  for sensor_id, sensor_value in sensor_values:
126  if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE] and (
127  val is None or val > sensor_value
128  ):
129  entity_id, val = sensor_id, sensor_value
130  return entity_id, val
131 
132 
133 def calc_max(sensor_values: list[tuple[str, Any]]) -> tuple[str | None, float | None]:
134  """Calculate max value, honoring unknown states."""
135  val: float | None = None
136  entity_id: str | None = None
137  for sensor_id, sensor_value in sensor_values:
138  if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE] and (
139  val is None or val < sensor_value
140  ):
141  entity_id, val = sensor_id, sensor_value
142  return entity_id, val
143 
144 
145 def calc_mean(sensor_values: list[tuple[str, Any]], round_digits: int) -> float | None:
146  """Calculate mean value, honoring unknown states."""
147  result = [
148  sensor_value
149  for _, sensor_value in sensor_values
150  if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE]
151  ]
152 
153  if not result:
154  return None
155  value: float = round(statistics.mean(result), round_digits)
156  return value
157 
158 
160  sensor_values: list[tuple[str, Any]], round_digits: int
161 ) -> float | None:
162  """Calculate median value, honoring unknown states."""
163  result = [
164  sensor_value
165  for _, sensor_value in sensor_values
166  if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE]
167  ]
168 
169  if not result:
170  return None
171  value: float = round(statistics.median(result), round_digits)
172  return value
173 
174 
175 def calc_range(sensor_values: list[tuple[str, Any]], round_digits: int) -> float | None:
176  """Calculate range value, honoring unknown states."""
177  result = [
178  sensor_value
179  for _, sensor_value in sensor_values
180  if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE]
181  ]
182 
183  if not result:
184  return None
185  value: float = round(max(result) - min(result), round_digits)
186  return value
187 
188 
189 def calc_sum(sensor_values: list[tuple[str, Any]], round_digits: int) -> float | None:
190  """Calculate a sum of values, not honoring unknown states."""
191  result = 0
192  for _, sensor_value in sensor_values:
193  if sensor_value in [STATE_UNKNOWN, STATE_UNAVAILABLE]:
194  return None
195  result += sensor_value
196 
197  value: float = round(result, round_digits)
198  return value
199 
200 
202  """Representation of a min/max sensor."""
203 
204  _attr_icon = ICON
205  _attr_should_poll = False
206  _attr_state_class = SensorStateClass.MEASUREMENT
207 
208  def __init__(
209  self,
210  entity_ids: list[str],
211  name: str | None,
212  sensor_type: str,
213  round_digits: int,
214  unique_id: str | None,
215  ) -> None:
216  """Initialize the min/max sensor."""
217  self._attr_unique_id_attr_unique_id = unique_id
218  self._entity_ids_entity_ids = entity_ids
219  self._sensor_type_sensor_type = sensor_type
220  self._round_digits_round_digits = round_digits
221 
222  if name:
223  self._attr_name_attr_name = name
224  else:
225  self._attr_name_attr_name = f"{sensor_type} sensor".capitalize()
226  self._sensor_attr_sensor_attr = SENSOR_TYPE_TO_ATTR[self._sensor_type_sensor_type]
227  self._unit_of_measurement_unit_of_measurement = None
228  self._unit_of_measurement_mismatch_unit_of_measurement_mismatch = False
229  self.min_valuemin_value: float | None = None
230  self.max_valuemax_value: float | None = None
231  self.meanmean: float | None = None
232  self.lastlast: float | None = None
233  self.medianmedian: float | None = None
234  self.rangerange: float | None = None
235  self.sumsum: float | None = None
236  self.min_entity_id: str | None = None
237  self.max_entity_id: str | None = None
238  self.last_entity_idlast_entity_id: str | None = None
239  self.count_sensorscount_sensors = len(self._entity_ids_entity_ids)
240  self.states: dict[str, Any] = {}
241 
242  async def async_added_to_hass(self) -> None:
243  """Handle added to Hass."""
244  self.async_on_removeasync_on_remove(
246  self.hasshass, self._entity_ids_entity_ids, self._async_min_max_sensor_state_listener_async_min_max_sensor_state_listener
247  )
248  )
249 
250  # Replay current state of source entities
251  for entity_id in self._entity_ids_entity_ids:
252  state = self.hasshass.states.get(entity_id)
253  state_event: Event[EventStateChangedData] = Event(
254  "", {"entity_id": entity_id, "new_state": state, "old_state": None}
255  )
256  self._async_min_max_sensor_state_listener_async_min_max_sensor_state_listener(state_event, update_state=False)
257 
258  self._calc_values_calc_values()
259 
260  @property
261  def native_value(self) -> StateType | datetime:
262  """Return the state of the sensor."""
263  if self._unit_of_measurement_mismatch_unit_of_measurement_mismatch:
264  return None
265  value: StateType | datetime = getattr(self, self._sensor_attr_sensor_attr)
266  return value
267 
268  @property
269  def native_unit_of_measurement(self) -> str | None:
270  """Return the unit the value is expressed in."""
271  if self._unit_of_measurement_mismatch_unit_of_measurement_mismatch:
272  return "ERR"
273  return self._unit_of_measurement_unit_of_measurement
274 
275  @property
276  def extra_state_attributes(self) -> dict[str, Any] | None:
277  """Return the state attributes of the sensor."""
278  if self._sensor_type_sensor_type == "min":
279  return {ATTR_MIN_ENTITY_ID: self.min_entity_id}
280  if self._sensor_type_sensor_type == "max":
281  return {ATTR_MAX_ENTITY_ID: self.max_entity_id}
282  if self._sensor_type_sensor_type == "last":
283  return {ATTR_LAST_ENTITY_ID: self.last_entity_idlast_entity_id}
284  return None
285 
286  @callback
288  self, event: Event[EventStateChangedData], update_state: bool = True
289  ) -> None:
290  """Handle the sensor state changes."""
291  new_state = event.data["new_state"]
292  entity = event.data["entity_id"]
293 
294  if (
295  new_state is None
296  or new_state.state is None
297  or new_state.state
298  in [
299  STATE_UNKNOWN,
300  STATE_UNAVAILABLE,
301  ]
302  ):
303  self.states[entity] = STATE_UNKNOWN
304  if not update_state:
305  return
306 
307  self._calc_values_calc_values()
308  self.async_write_ha_stateasync_write_ha_state()
309  return
310 
311  if self._unit_of_measurement_unit_of_measurement is None:
312  self._unit_of_measurement_unit_of_measurement = new_state.attributes.get(
313  ATTR_UNIT_OF_MEASUREMENT
314  )
315 
316  if self._unit_of_measurement_unit_of_measurement != new_state.attributes.get(
317  ATTR_UNIT_OF_MEASUREMENT
318  ):
319  _LOGGER.warning(
320  "Units of measurement do not match for entity %s", self.entity_identity_id
321  )
322  self._unit_of_measurement_mismatch_unit_of_measurement_mismatch = True
323 
324  try:
325  self.states[entity] = float(new_state.state)
326  self.lastlast = float(new_state.state)
327  self.last_entity_idlast_entity_id = entity
328  except ValueError:
329  _LOGGER.warning(
330  "Unable to store state. Only numerical states are supported"
331  )
332 
333  if not update_state:
334  return
335 
336  self._calc_values_calc_values()
337  self.async_write_ha_stateasync_write_ha_state()
338 
339  @callback
340  def _calc_values(self) -> None:
341  """Calculate the values."""
342  sensor_values = [
343  (entity_id, self.states[entity_id])
344  for entity_id in self._entity_ids_entity_ids
345  if entity_id in self.states
346  ]
347  self.min_entity_id, self.min_valuemin_value = calc_min(sensor_values)
348  self.max_entity_id, self.max_valuemax_value = calc_max(sensor_values)
349  self.meanmean = calc_mean(sensor_values, self._round_digits_round_digits)
350  self.medianmedian = calc_median(sensor_values, self._round_digits_round_digits)
351  self.rangerange = calc_range(sensor_values, self._round_digits_round_digits)
352  self.sumsum = calc_sum(sensor_values, self._round_digits_round_digits)
dict[str, Any]|None extra_state_attributes(self)
Definition: sensor.py:276
None _async_min_max_sensor_state_listener(self, Event[EventStateChangedData] event, bool update_state=True)
Definition: sensor.py:289
None __init__(self, list[str] entity_ids, str|None name, str sensor_type, int round_digits, str|None unique_id)
Definition: sensor.py:215
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
float|None calc_median(list[tuple[str, Any]] sensor_values, int round_digits)
Definition: sensor.py:161
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:79
float|None calc_range(list[tuple[str, Any]] sensor_values, int round_digits)
Definition: sensor.py:175
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: sensor.py:106
float|None calc_sum(list[tuple[str, Any]] sensor_values, int round_digits)
Definition: sensor.py:189
tuple[str|None, float|None] calc_max(list[tuple[str, Any]] sensor_values)
Definition: sensor.py:133
tuple[str|None, float|None] calc_min(list[tuple[str, Any]] sensor_values)
Definition: sensor.py:121
float|None calc_mean(list[tuple[str, Any]] sensor_values, int round_digits)
Definition: sensor.py:145
CALLBACK_TYPE async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)
Definition: event.py:314
None async_setup_reload_service(HomeAssistant hass, str domain, Iterable[str] platforms)
Definition: reload.py:191