Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Allows the creation of a sensor that filters state property."""
2 
3 from __future__ import annotations
4 
5 from collections import Counter, deque
6 from copy import copy
7 from dataclasses import dataclass
8 from datetime import datetime, timedelta
9 from functools import partial
10 import logging
11 from numbers import Number
12 import statistics
13 from typing import Any, cast
14 
15 import voluptuous as vol
16 
17 from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
18 from homeassistant.components.input_number import DOMAIN as INPUT_NUMBER_DOMAIN
19 from homeassistant.components.recorder import get_instance, history
21  ATTR_STATE_CLASS,
22  DOMAIN as SENSOR_DOMAIN,
23  PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
24  SensorDeviceClass,
25  SensorEntity,
26 )
27 from homeassistant.const import (
28  ATTR_DEVICE_CLASS,
29  ATTR_ENTITY_ID,
30  ATTR_ICON,
31  ATTR_UNIT_OF_MEASUREMENT,
32  CONF_ENTITY_ID,
33  CONF_NAME,
34  CONF_UNIQUE_ID,
35  STATE_UNAVAILABLE,
36  STATE_UNKNOWN,
37 )
38 from homeassistant.core import (
39  Event,
40  EventStateChangedData,
41  HomeAssistant,
42  State,
43  callback,
44 )
46 from homeassistant.helpers.entity_platform import AddEntitiesCallback
47 from homeassistant.helpers.event import async_track_state_change_event
48 from homeassistant.helpers.reload import async_setup_reload_service
49 from homeassistant.helpers.start import async_at_started
50 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
51 from homeassistant.util.decorator import Registry
52 import homeassistant.util.dt as dt_util
53 
54 from . import DOMAIN, PLATFORMS
55 
56 _LOGGER = logging.getLogger(__name__)
57 
58 FILTER_NAME_RANGE = "range"
59 FILTER_NAME_LOWPASS = "lowpass"
60 FILTER_NAME_OUTLIER = "outlier"
61 FILTER_NAME_THROTTLE = "throttle"
62 FILTER_NAME_TIME_THROTTLE = "time_throttle"
63 FILTER_NAME_TIME_SMA = "time_simple_moving_average"
64 FILTERS: Registry[str, type[Filter]] = Registry()
65 
66 CONF_FILTERS = "filters"
67 CONF_FILTER_NAME = "filter"
68 CONF_FILTER_WINDOW_SIZE = "window_size"
69 CONF_FILTER_PRECISION = "precision"
70 CONF_FILTER_RADIUS = "radius"
71 CONF_FILTER_TIME_CONSTANT = "time_constant"
72 CONF_FILTER_LOWER_BOUND = "lower_bound"
73 CONF_FILTER_UPPER_BOUND = "upper_bound"
74 CONF_TIME_SMA_TYPE = "type"
75 
76 TIME_SMA_LAST = "last"
77 
78 WINDOW_SIZE_UNIT_NUMBER_EVENTS = 1
79 WINDOW_SIZE_UNIT_TIME = 2
80 
81 DEFAULT_WINDOW_SIZE = 1
82 DEFAULT_PRECISION = 2
83 DEFAULT_FILTER_RADIUS = 2.0
84 DEFAULT_FILTER_TIME_CONSTANT = 10
85 
86 NAME_TEMPLATE = "{} filter"
87 ICON = "mdi:chart-line-variant"
88 
89 FILTER_SCHEMA = vol.Schema({vol.Optional(CONF_FILTER_PRECISION): vol.Coerce(int)})
90 
91 FILTER_OUTLIER_SCHEMA = FILTER_SCHEMA.extend(
92  {
93  vol.Required(CONF_FILTER_NAME): FILTER_NAME_OUTLIER,
94  vol.Optional(CONF_FILTER_WINDOW_SIZE, default=DEFAULT_WINDOW_SIZE): vol.Coerce(
95  int
96  ),
97  vol.Optional(CONF_FILTER_RADIUS, default=DEFAULT_FILTER_RADIUS): vol.Coerce(
98  float
99  ),
100  }
101 )
102 
103 FILTER_LOWPASS_SCHEMA = FILTER_SCHEMA.extend(
104  {
105  vol.Required(CONF_FILTER_NAME): FILTER_NAME_LOWPASS,
106  vol.Optional(CONF_FILTER_WINDOW_SIZE, default=DEFAULT_WINDOW_SIZE): vol.Coerce(
107  int
108  ),
109  vol.Optional(
110  CONF_FILTER_TIME_CONSTANT, default=DEFAULT_FILTER_TIME_CONSTANT
111  ): vol.Coerce(int),
112  }
113 )
114 
115 FILTER_RANGE_SCHEMA = FILTER_SCHEMA.extend(
116  {
117  vol.Required(CONF_FILTER_NAME): FILTER_NAME_RANGE,
118  vol.Optional(CONF_FILTER_LOWER_BOUND): vol.Coerce(float),
119  vol.Optional(CONF_FILTER_UPPER_BOUND): vol.Coerce(float),
120  }
121 )
122 
123 FILTER_TIME_SMA_SCHEMA = FILTER_SCHEMA.extend(
124  {
125  vol.Required(CONF_FILTER_NAME): FILTER_NAME_TIME_SMA,
126  vol.Optional(CONF_TIME_SMA_TYPE, default=TIME_SMA_LAST): vol.In(
127  [TIME_SMA_LAST]
128  ),
129  vol.Required(CONF_FILTER_WINDOW_SIZE): vol.All(
130  cv.time_period, cv.positive_timedelta
131  ),
132  }
133 )
134 
135 FILTER_THROTTLE_SCHEMA = FILTER_SCHEMA.extend(
136  {
137  vol.Required(CONF_FILTER_NAME): FILTER_NAME_THROTTLE,
138  vol.Optional(CONF_FILTER_WINDOW_SIZE, default=DEFAULT_WINDOW_SIZE): vol.Coerce(
139  int
140  ),
141  }
142 )
143 
144 FILTER_TIME_THROTTLE_SCHEMA = FILTER_SCHEMA.extend(
145  {
146  vol.Required(CONF_FILTER_NAME): FILTER_NAME_TIME_THROTTLE,
147  vol.Required(CONF_FILTER_WINDOW_SIZE): vol.All(
148  cv.time_period, cv.positive_timedelta
149  ),
150  }
151 )
152 
153 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
154  {
155  vol.Required(CONF_ENTITY_ID): vol.Any(
156  cv.entity_domain(SENSOR_DOMAIN),
157  cv.entity_domain(BINARY_SENSOR_DOMAIN),
158  cv.entity_domain(INPUT_NUMBER_DOMAIN),
159  ),
160  vol.Optional(CONF_NAME): cv.string,
161  vol.Optional(CONF_UNIQUE_ID): cv.string,
162  vol.Required(CONF_FILTERS): vol.All(
163  cv.ensure_list,
164  [
165  vol.Any(
166  FILTER_OUTLIER_SCHEMA,
167  FILTER_LOWPASS_SCHEMA,
168  FILTER_TIME_SMA_SCHEMA,
169  FILTER_THROTTLE_SCHEMA,
170  FILTER_TIME_THROTTLE_SCHEMA,
171  FILTER_RANGE_SCHEMA,
172  )
173  ],
174  ),
175  }
176 )
177 
178 
180  hass: HomeAssistant,
181  config: ConfigType,
182  async_add_entities: AddEntitiesCallback,
183  discovery_info: DiscoveryInfoType | None = None,
184 ) -> None:
185  """Set up the template sensors."""
186 
187  await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
188 
189  name: str | None = config.get(CONF_NAME)
190  unique_id: str | None = config.get(CONF_UNIQUE_ID)
191  entity_id: str = config[CONF_ENTITY_ID]
192 
193  filter_configs: list[dict[str, Any]] = config[CONF_FILTERS]
194  filters = [
195  FILTERS[_filter.pop(CONF_FILTER_NAME)](entity=entity_id, **_filter)
196  for _filter in filter_configs
197  ]
198 
199  async_add_entities([SensorFilter(name, unique_id, entity_id, filters)])
200 
201 
203  """Representation of a Filter Sensor."""
204 
205  _attr_should_poll = False
206 
207  def __init__(
208  self,
209  name: str | None,
210  unique_id: str | None,
211  entity_id: str,
212  filters: list[Filter],
213  ) -> None:
214  """Initialize the sensor."""
215  self._attr_name_attr_name = name
216  self._attr_unique_id_attr_unique_id = unique_id
217  self._entity_entity = entity_id
218  self._attr_native_unit_of_measurement_attr_native_unit_of_measurement = None
219  self._state_state: StateType = None
220  self._filters_filters = filters
221  self._attr_icon_attr_icon = None
222  self._attr_device_class_attr_device_class = None
223  self._attr_state_class_attr_state_class = None
224  self._attr_extra_state_attributes_attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_id}
225 
226  @callback
228  self, event: Event[EventStateChangedData]
229  ) -> None:
230  """Handle device state changes."""
231  _LOGGER.debug("Update filter on event: %s", event)
232  self._update_filter_sensor_state_update_filter_sensor_state(event.data["new_state"])
233 
234  @callback
236  self, new_state: State | None, update_ha: bool = True
237  ) -> None:
238  """Process device state changes."""
239  if new_state is None:
240  _LOGGER.warning(
241  "While updating filter %s, the new_state is None", self.namename
242  )
243  self._state_state = None
244  self.async_write_ha_stateasync_write_ha_state()
245  return
246 
247  if new_state.state == STATE_UNKNOWN:
248  self._state_state = None
249  self.async_write_ha_stateasync_write_ha_state()
250  return
251 
252  if new_state.state == STATE_UNAVAILABLE:
253  self._attr_available_attr_available = False
254  self.async_write_ha_stateasync_write_ha_state()
255  return
256 
257  self._attr_available_attr_available = True
258 
259  temp_state = _State(new_state.last_updated, new_state.state)
260 
261  try:
262  for filt in self._filters_filters:
263  filtered_state = filt.filter_state(copy(temp_state))
264  _LOGGER.debug(
265  "%s(%s=%s) -> %s",
266  filt.name,
267  self._entity_entity,
268  temp_state.state,
269  "skip" if filt.skip_processing else filtered_state.state,
270  )
271  if filt.skip_processing:
272  return
273  temp_state = filtered_state
274  except ValueError:
275  _LOGGER.error(
276  "Could not convert state: %s (%s) to number",
277  new_state.state,
278  type(new_state.state),
279  )
280  return
281 
282  self._state_state = temp_state.state
283 
284  self._attr_icon_attr_icon = new_state.attributes.get(ATTR_ICON, ICON)
285  self._attr_device_class_attr_device_class = new_state.attributes.get(ATTR_DEVICE_CLASS)
286  self._attr_state_class_attr_state_class = new_state.attributes.get(ATTR_STATE_CLASS)
287 
288  if self._attr_native_unit_of_measurement_attr_native_unit_of_measurement != new_state.attributes.get(
289  ATTR_UNIT_OF_MEASUREMENT
290  ):
291  for filt in self._filters_filters:
292  filt.reset()
293  self._attr_native_unit_of_measurement_attr_native_unit_of_measurement = new_state.attributes.get(
294  ATTR_UNIT_OF_MEASUREMENT
295  )
296 
297  if update_ha:
298  self.async_write_ha_stateasync_write_ha_state()
299 
300  async def async_added_to_hass(self) -> None:
301  """Register callbacks."""
302 
303  if "recorder" in self.hasshass.config.components:
304  history_list = []
305  largest_window_items = 0
306  largest_window_time = timedelta(0)
307 
308  # Determine the largest window_size by type
309  for filt in self._filters_filters:
310  if (
311  filt.window_unit == WINDOW_SIZE_UNIT_NUMBER_EVENTS
312  and largest_window_items < (size := cast(int, filt.window_size))
313  ):
314  largest_window_items = size
315  elif (
316  filt.window_unit == WINDOW_SIZE_UNIT_TIME
317  and largest_window_time < (val := cast(timedelta, filt.window_size))
318  ):
319  largest_window_time = val
320 
321  # Retrieve the largest window_size of each type
322  if largest_window_items > 0:
323  filter_history = await get_instance(self.hasshass).async_add_executor_job(
324  partial(
325  history.get_last_state_changes,
326  self.hasshass,
327  largest_window_items,
328  entity_id=self._entity_entity,
329  )
330  )
331  if self._entity_entity in filter_history:
332  history_list.extend(filter_history[self._entity_entity])
333  if largest_window_time > timedelta(seconds=0):
334  start = dt_util.utcnow() - largest_window_time
335  filter_history = await get_instance(self.hasshass).async_add_executor_job(
336  partial(
337  history.state_changes_during_period,
338  self.hasshass,
339  start,
340  entity_id=self._entity_entity,
341  )
342  )
343  if self._entity_entity in filter_history:
344  history_list.extend(
345  [
346  state
347  for state in filter_history[self._entity_entity]
348  if state not in history_list
349  ]
350  )
351 
352  # Sort the window states
353  history_list = sorted(history_list, key=lambda s: s.last_updated)
354  _LOGGER.debug(
355  "Loading from history: %s",
356  [(s.state, s.last_updated) for s in history_list],
357  )
358 
359  # Replay history through the filter chain
360  for state in history_list:
361  if state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE, None]:
362  self._update_filter_sensor_state_update_filter_sensor_state(state, False)
363 
364  @callback
365  def _async_hass_started(hass: HomeAssistant) -> None:
366  """Delay source entity tracking."""
367  self.async_on_removeasync_on_remove(
369  self.hasshass, [self._entity_entity], self._update_filter_sensor_state_event_update_filter_sensor_state_event
370  )
371  )
372 
373  self.async_on_removeasync_on_remove(async_at_started(self.hasshass, _async_hass_started))
374 
375  @property
376  def native_value(self) -> datetime | StateType:
377  """Return the state of the sensor."""
378  if self._state_state is not None and self.device_classdevice_classdevice_classdevice_class == SensorDeviceClass.TIMESTAMP:
379  return datetime.fromisoformat(str(self._state_state))
380 
381  return self._state_state
382 
383 
385  """State abstraction for filter usage."""
386 
387  state: str | float | int
388 
389  def __init__(self, state: _State) -> None:
390  """Initialize with HA State object."""
391  self.timestamptimestamp = state.last_updated
392  try:
393  self.statestate = float(state.state)
394  except ValueError:
395  self.statestate = state.state
396 
397  def set_precision(self, precision: int | None) -> None:
398  """Set precision of Number based states."""
399  if precision is not None and isinstance(self.statestate, Number):
400  value = round(float(self.statestate), precision)
401  self.statestate = int(value) if precision == 0 else value
402 
403  def __str__(self) -> str:
404  """Return state as the string representation of FilterState."""
405  return str(self.statestate)
406 
407  def __repr__(self) -> str:
408  """Return timestamp and state as the representation of FilterState."""
409  return f"{self.timestamp} : {self.state}"
410 
411 
412 @dataclass
413 class _State:
414  """Simplified State class.
415 
416  The standard State class only accepts string in `state`,
417  and we are only interested in two properties.
418  """
419 
420  last_updated: datetime
421  state: str | float | int
422 
423 
424 class Filter:
425  """Filter skeleton."""
426 
427  def __init__(
428  self,
429  name: str,
430  window_size: int | timedelta,
431  entity: str,
432  precision: int | None,
433  ) -> None:
434  """Initialize common attributes.
435 
436  :param window_size: size of the sliding window that holds previous values
437  :param precision: round filtered value to precision value
438  :param entity: used for debugging only
439  """
440  if isinstance(window_size, int):
441  self.statesstates: deque[FilterState] = deque(maxlen=window_size)
442  self.window_unitwindow_unit = WINDOW_SIZE_UNIT_NUMBER_EVENTS
443  else:
444  self.statesstates = deque(maxlen=0)
445  self.window_unitwindow_unit = WINDOW_SIZE_UNIT_TIME
446  self.filter_precisionfilter_precision = precision
447  self._name_name = name
448  self._entity_entity = entity
449  self._skip_processing_skip_processing = False
450  self._window_size_window_size = window_size
451  self._store_raw_store_raw = False
452  self._only_numbers_only_numbers = True
453 
454  @property
455  def window_size(self) -> int | timedelta:
456  """Return window size."""
457  return self._window_size_window_size
458 
459  @property
460  def name(self) -> str:
461  """Return filter name."""
462  return self._name_name
463 
464  @property
465  def skip_processing(self) -> bool:
466  """Return whether the current filter_state should be skipped."""
467  return self._skip_processing_skip_processing
468 
469  def reset(self) -> None:
470  """Reset filter."""
471  self.statesstates.clear()
472 
473  def _filter_state(self, new_state: FilterState) -> FilterState:
474  """Implement filter."""
475  raise NotImplementedError
476 
477  def filter_state(self, new_state: _State) -> _State:
478  """Implement a common interface for filters."""
479  fstate = FilterState(new_state)
480  if self._only_numbers_only_numbers and not isinstance(fstate.state, Number):
481  raise ValueError(f"State <{fstate.state}> is not a Number")
482 
483  filtered = self._filter_state_filter_state(fstate)
484  filtered.set_precision(self.filter_precisionfilter_precision)
485 
486  if self._store_raw_store_raw:
487  self.statesstates.append(copy(FilterState(new_state)))
488  else:
489  self.statesstates.append(copy(filtered))
490  new_state.state = filtered.state
491  return new_state
492 
493 
494 @FILTERS.register(FILTER_NAME_RANGE)
496  """Range filter.
497 
498  Determines if new state is in the range of upper_bound and lower_bound.
499  If not inside, lower or upper bound is returned instead.
500  """
501 
502  def __init__(
503  self,
504  *,
505  entity: str,
506  precision: int | None = None,
507  lower_bound: float | None = None,
508  upper_bound: float | None = None,
509  ) -> None:
510  """Initialize Filter.
511 
512  :param upper_bound: band upper bound
513  :param lower_bound: band lower bound
514  """
515  super().__init__(
516  FILTER_NAME_RANGE, DEFAULT_WINDOW_SIZE, precision=precision, entity=entity
517  )
518  self._lower_bound_lower_bound = lower_bound
519  self._upper_bound_upper_bound = upper_bound
520  self._stats_internal: Counter = Counter()
521 
522  def _filter_state(self, new_state: FilterState) -> FilterState:
523  """Implement the range filter."""
524 
525  # We can cast safely here thanks to self._only_numbers = True
526  new_state_value = cast(float, new_state.state)
527 
528  if self._upper_bound_upper_bound is not None and new_state_value > self._upper_bound_upper_bound:
529  self._stats_internal["erasures_up"] += 1
530 
531  _LOGGER.debug(
532  "Upper outlier nr. %s in %s: %s",
533  self._stats_internal["erasures_up"],
534  self._entity_entity,
535  new_state,
536  )
537  new_state.state = self._upper_bound_upper_bound
538 
539  elif self._lower_bound_lower_bound is not None and new_state_value < self._lower_bound_lower_bound:
540  self._stats_internal["erasures_low"] += 1
541 
542  _LOGGER.debug(
543  "Lower outlier nr. %s in %s: %s",
544  self._stats_internal["erasures_low"],
545  self._entity_entity,
546  new_state,
547  )
548  new_state.state = self._lower_bound_lower_bound
549 
550  return new_state
551 
552 
553 @FILTERS.register(FILTER_NAME_OUTLIER)
555  """BASIC outlier filter.
556 
557  Determines if new state is in a band around the median.
558  """
559 
560  def __init__(
561  self,
562  *,
563  window_size: int,
564  entity: str,
565  radius: float,
566  precision: int | None = None,
567  ) -> None:
568  """Initialize Filter.
569 
570  :param radius: band radius
571  """
572  super().__init__(
573  FILTER_NAME_OUTLIER, window_size, precision=precision, entity=entity
574  )
575  self._radius_radius = radius
576  self._stats_internal: Counter = Counter()
577  self._store_raw_store_raw_store_raw = True
578 
579  def _filter_state(self, new_state: FilterState) -> FilterState:
580  """Implement the outlier filter."""
581 
582  # We can cast safely here thanks to self._only_numbers = True
583  previous_state_values = [cast(float, s.state) for s in self.statesstates]
584  new_state_value = cast(float, new_state.state)
585 
586  median = statistics.median(previous_state_values) if self.statesstates else 0
587  if (
588  len(self.statesstates) == self.statesstates.maxlen
589  and abs(new_state_value - median) > self._radius_radius
590  ):
591  self._stats_internal["erasures"] += 1
592 
593  _LOGGER.debug(
594  "Outlier nr. %s in %s: %s",
595  self._stats_internal["erasures"],
596  self._entity_entity,
597  new_state,
598  )
599  new_state.state = median
600  return new_state
601 
602 
603 @FILTERS.register(FILTER_NAME_LOWPASS)
605  """BASIC Low Pass Filter."""
606 
607  def __init__(
608  self,
609  *,
610  window_size: int,
611  entity: str,
612  time_constant: int,
613  precision: int = DEFAULT_PRECISION,
614  ) -> None:
615  """Initialize Filter."""
616  super().__init__(
617  FILTER_NAME_LOWPASS, window_size, precision=precision, entity=entity
618  )
619  self._time_constant_time_constant = time_constant
620 
621  def _filter_state(self, new_state: FilterState) -> FilterState:
622  """Implement the low pass filter."""
623 
624  if not self.statesstates:
625  return new_state
626 
627  new_weight = 1.0 / self._time_constant_time_constant
628  prev_weight = 1.0 - new_weight
629  # We can cast safely here thanks to self._only_numbers = True
630  prev_state_value = cast(float, self.statesstates[-1].state)
631  new_state_value = cast(float, new_state.state)
632  new_state.state = prev_weight * prev_state_value + new_weight * new_state_value
633 
634  return new_state
635 
636 
637 @FILTERS.register(FILTER_NAME_TIME_SMA)
639  """Simple Moving Average (SMA) Filter.
640 
641  The window_size is determined by time, and SMA is time weighted.
642  """
643 
644  def __init__(
645  self,
646  *,
647  window_size: timedelta,
648  entity: str,
649  type: str, # pylint: disable=redefined-builtin
650  precision: int = DEFAULT_PRECISION,
651  ) -> None:
652  """Initialize Filter.
653 
654  :param type: type of algorithm used to connect discrete values
655  """
656  super().__init__(
657  FILTER_NAME_TIME_SMA, window_size, precision=precision, entity=entity
658  )
659  self._time_window_time_window = window_size
660  self.last_leaklast_leak: FilterState | None = None
661  self.queuequeue = deque[FilterState]()
662 
663  def _leak(self, left_boundary: datetime) -> None:
664  """Remove timeouted elements."""
665  while self.queuequeue:
666  if self.queuequeue[0].timestamp + self._time_window_time_window <= left_boundary:
667  self.last_leaklast_leak = self.queuequeue.popleft()
668  else:
669  return
670 
671  def _filter_state(self, new_state: FilterState) -> FilterState:
672  """Implement the Simple Moving Average filter."""
673 
674  self._leak_leak(new_state.timestamp)
675  self.queuequeue.append(copy(new_state))
676 
677  moving_sum: float = 0
678  start = new_state.timestamp - self._time_window_time_window
679  prev_state = self.last_leaklast_leak if self.last_leaklast_leak is not None else self.queuequeue[0]
680  for state in self.queuequeue:
681  # We can cast safely here thanks to self._only_numbers = True
682  prev_state_value = cast(float, prev_state.state)
683  moving_sum += (state.timestamp - start).total_seconds() * prev_state_value
684  start = state.timestamp
685  prev_state = state
686 
687  new_state.state = moving_sum / self._time_window_time_window.total_seconds()
688 
689  return new_state
690 
691 
692 @FILTERS.register(FILTER_NAME_THROTTLE)
694  """Throttle Filter.
695 
696  One sample per window.
697  """
698 
699  def __init__(
700  self, *, window_size: int, entity: str, precision: None = None
701  ) -> None:
702  """Initialize Filter."""
703  super().__init__(
704  FILTER_NAME_THROTTLE, window_size, precision=precision, entity=entity
705  )
706  self._only_numbers_only_numbers_only_numbers = False
707 
708  def _filter_state(self, new_state: FilterState) -> FilterState:
709  """Implement the throttle filter."""
710  if not self.statesstates or len(self.statesstates) == self.statesstates.maxlen:
711  self.statesstates.clear()
712  self._skip_processing_skip_processing_skip_processing = False
713  else:
714  self._skip_processing_skip_processing_skip_processing = True
715 
716  return new_state
717 
718 
719 @FILTERS.register(FILTER_NAME_TIME_THROTTLE)
721  """Time Throttle Filter.
722 
723  One sample per time period.
724  """
725 
726  def __init__(
727  self, *, window_size: timedelta, entity: str, precision: int | None = None
728  ) -> None:
729  """Initialize Filter."""
730  super().__init__(
731  FILTER_NAME_TIME_THROTTLE, window_size, precision=precision, entity=entity
732  )
733  self._time_window_time_window = window_size
734  self._last_emitted_at_last_emitted_at: datetime | None = None
735  self._only_numbers_only_numbers_only_numbers = False
736 
737  def _filter_state(self, new_state: FilterState) -> FilterState:
738  """Implement the filter."""
739  window_start = new_state.timestamp - self._time_window_time_window
740  if not self._last_emitted_at_last_emitted_at or self._last_emitted_at_last_emitted_at <= window_start:
741  self._last_emitted_at_last_emitted_at = new_state.timestamp
742  self._skip_processing_skip_processing_skip_processing = False
743  else:
744  self._skip_processing_skip_processing_skip_processing = True
745 
746  return new_state
None set_precision(self, int|None precision)
Definition: sensor.py:397
FilterState _filter_state(self, FilterState new_state)
Definition: sensor.py:473
_State filter_state(self, _State new_state)
Definition: sensor.py:477
None __init__(self, str name, int|timedelta window_size, str entity, int|None precision)
Definition: sensor.py:433
FilterState _filter_state(self, FilterState new_state)
Definition: sensor.py:621
None __init__(self, *int window_size, str entity, int time_constant, int precision=DEFAULT_PRECISION)
Definition: sensor.py:614
None __init__(self, *int window_size, str entity, float radius, int|None precision=None)
Definition: sensor.py:567
FilterState _filter_state(self, FilterState new_state)
Definition: sensor.py:579
None __init__(self, *str entity, int|None precision=None, float|None lower_bound=None, float|None upper_bound=None)
Definition: sensor.py:509
FilterState _filter_state(self, FilterState new_state)
Definition: sensor.py:522
None _update_filter_sensor_state_event(self, Event[EventStateChangedData] event)
Definition: sensor.py:229
None __init__(self, str|None name, str|None unique_id, str entity_id, list[Filter] filters)
Definition: sensor.py:213
None _update_filter_sensor_state(self, State|None new_state, bool update_ha=True)
Definition: sensor.py:237
None __init__(self, *int window_size, str entity, None precision=None)
Definition: sensor.py:701
FilterState _filter_state(self, FilterState new_state)
Definition: sensor.py:708
None _leak(self, datetime left_boundary)
Definition: sensor.py:663
None __init__(self, *timedelta window_size, str entity, str type, int precision=DEFAULT_PRECISION)
Definition: sensor.py:651
FilterState _filter_state(self, FilterState new_state)
Definition: sensor.py:671
FilterState _filter_state(self, FilterState new_state)
Definition: sensor.py:737
None __init__(self, *timedelta window_size, str entity, int|None precision=None)
Definition: sensor.py:728
SensorDeviceClass|None device_class(self)
Definition: __init__.py:313
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
str|UndefinedType|None name(self)
Definition: entity.py:738
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: sensor.py:184
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
Recorder get_instance(HomeAssistant hass)
Definition: recorder.py:74
None async_setup_reload_service(HomeAssistant hass, str domain, Iterable[str] platforms)
Definition: reload.py:191
CALLBACK_TYPE async_at_started(HomeAssistant hass, Callable[[HomeAssistant], Coroutine[Any, Any, None]|None] at_start_cb)
Definition: start.py:80