Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Component to make instant statistics about your history."""
2 
3 from __future__ import annotations
4 
5 from abc import abstractmethod
6 import datetime
7 from typing import Any
8 
9 import voluptuous as vol
10 
12  PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
13  SensorDeviceClass,
14  SensorEntity,
15  SensorStateClass,
16 )
17 from homeassistant.const import (
18  CONF_ENTITY_ID,
19  CONF_NAME,
20  CONF_STATE,
21  CONF_TYPE,
22  CONF_UNIQUE_ID,
23  PERCENTAGE,
24  UnitOfTime,
25 )
26 from homeassistant.core import HomeAssistant, callback
27 from homeassistant.exceptions import PlatformNotReady
29 from homeassistant.helpers.device import async_device_info_to_link_from_entity
30 from homeassistant.helpers.entity_platform import AddEntitiesCallback
31 from homeassistant.helpers.reload import async_setup_reload_service
32 from homeassistant.helpers.template import Template
33 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
34 from homeassistant.helpers.update_coordinator import CoordinatorEntity
35 
36 from . import HistoryStatsConfigEntry
37 from .const import (
38  CONF_DURATION,
39  CONF_END,
40  CONF_PERIOD_KEYS,
41  CONF_START,
42  CONF_TYPE_COUNT,
43  CONF_TYPE_KEYS,
44  CONF_TYPE_RATIO,
45  CONF_TYPE_TIME,
46  DEFAULT_NAME,
47  DOMAIN,
48  PLATFORMS,
49 )
50 from .coordinator import HistoryStatsUpdateCoordinator
51 from .data import HistoryStats
52 from .helpers import pretty_ratio
53 
54 UNITS: dict[str, str] = {
55  CONF_TYPE_TIME: UnitOfTime.HOURS,
56  CONF_TYPE_RATIO: PERCENTAGE,
57  CONF_TYPE_COUNT: "",
58 }
59 ICON = "mdi:chart-line"
60 
61 
62 def exactly_two_period_keys[_T: dict[str, Any]](conf: _T) -> _T:
63  """Ensure exactly 2 of CONF_PERIOD_KEYS are provided."""
64  if sum(param in conf for param in CONF_PERIOD_KEYS) != 2:
65  raise vol.Invalid(
66  "You must provide exactly 2 of the following: start, end, duration"
67  )
68  return conf
69 
70 
71 PLATFORM_SCHEMA = vol.All(
72  SENSOR_PLATFORM_SCHEMA.extend(
73  {
74  vol.Required(CONF_ENTITY_ID): cv.entity_id,
75  vol.Required(CONF_STATE): vol.All(cv.ensure_list, [cv.string]),
76  vol.Optional(CONF_START): cv.template,
77  vol.Optional(CONF_END): cv.template,
78  vol.Optional(CONF_DURATION): cv.time_period,
79  vol.Optional(CONF_TYPE, default=CONF_TYPE_TIME): vol.In(CONF_TYPE_KEYS),
80  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
81  vol.Optional(CONF_UNIQUE_ID): cv.string,
82  }
83  ),
84  exactly_two_period_keys,
85 )
86 
87 
89  hass: HomeAssistant,
90  config: ConfigType,
91  async_add_entities: AddEntitiesCallback,
92  discovery_info: DiscoveryInfoType | None = None,
93 ) -> None:
94  """Set up the History Stats sensor."""
95  await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
96 
97  entity_id: str = config[CONF_ENTITY_ID]
98  entity_states: list[str] = config[CONF_STATE]
99  start: Template | None = config.get(CONF_START)
100  end: Template | None = config.get(CONF_END)
101  duration: datetime.timedelta | None = config.get(CONF_DURATION)
102  sensor_type: str = config[CONF_TYPE]
103  name: str = config[CONF_NAME]
104  unique_id: str | None = config.get(CONF_UNIQUE_ID)
105 
106  history_stats = HistoryStats(hass, entity_id, entity_states, start, end, duration)
107  coordinator = HistoryStatsUpdateCoordinator(hass, history_stats, None, name)
108  await coordinator.async_refresh()
109  if not coordinator.last_update_success:
110  raise PlatformNotReady from coordinator.last_exception
112  [HistoryStatsSensor(hass, coordinator, sensor_type, name, unique_id, entity_id)]
113  )
114 
115 
117  hass: HomeAssistant,
118  entry: HistoryStatsConfigEntry,
119  async_add_entities: AddEntitiesCallback,
120 ) -> None:
121  """Set up the History stats sensor entry."""
122 
123  sensor_type: str = entry.options[CONF_TYPE]
124  coordinator = entry.runtime_data
125  entity_id: str = entry.options[CONF_ENTITY_ID]
127  [
129  hass, coordinator, sensor_type, entry.title, entry.entry_id, entity_id
130  )
131  ]
132  )
133 
134 
136  CoordinatorEntity[HistoryStatsUpdateCoordinator], SensorEntity
137 ):
138  """Base class for a HistoryStats sensor."""
139 
140  _attr_icon = ICON
141 
142  def __init__(
143  self,
144  coordinator: HistoryStatsUpdateCoordinator,
145  name: str,
146  ) -> None:
147  """Initialize the HistoryStats sensor base class."""
148  super().__init__(coordinator)
149  self._attr_name_attr_name = name
150 
151  async def async_added_to_hass(self) -> None:
152  """Entity has been added to hass."""
153  await super().async_added_to_hass()
154  self.async_on_removeasync_on_remove(self.coordinator.async_setup_state_listener())
155 
156  def _handle_coordinator_update(self) -> None:
157  """Set attrs from value and count."""
158  self._process_update_process_update()
160 
161  @callback
162  @abstractmethod
163  def _process_update(self) -> None:
164  """Process an update from the coordinator."""
165 
166 
167 class HistoryStatsSensor(HistoryStatsSensorBase):
168  """A HistoryStats sensor."""
169 
170  _attr_state_class = SensorStateClass.MEASUREMENT
171 
172  def __init__(
173  self,
174  hass: HomeAssistant,
175  coordinator: HistoryStatsUpdateCoordinator,
176  sensor_type: str,
177  name: str,
178  unique_id: str | None,
179  source_entity_id: str,
180  ) -> None:
181  """Initialize the HistoryStats sensor."""
182  super().__init__(coordinator, name)
183  self._attr_native_unit_of_measurement_attr_native_unit_of_measurement = UNITS[sensor_type]
184  self._type_type = sensor_type
185  self._attr_unique_id_attr_unique_id = unique_id
187  hass,
188  source_entity_id,
189  )
190  self._process_update_process_update_process_update()
191  if self._type_type == CONF_TYPE_TIME:
192  self._attr_device_class_attr_device_class = SensorDeviceClass.DURATION
193  self._attr_suggested_display_precision_attr_suggested_display_precision = 2
194 
195  @callback
196  def _process_update(self) -> None:
197  """Process an update from the coordinator."""
198  state = self.coordinator.data
199  if state is None or state.seconds_matched is None:
200  self._attr_native_value_attr_native_value = None
201  return
202 
203  if self._type_type == CONF_TYPE_TIME:
204  value = state.seconds_matched / 3600
205  if self._attr_unique_id_attr_unique_id is None:
206  value = round(value, 2)
207  self._attr_native_value_attr_native_value = value
208  elif self._type_type == CONF_TYPE_RATIO:
209  self._attr_native_value_attr_native_value = pretty_ratio(state.seconds_matched, state.period)
210  elif self._type_type == CONF_TYPE_COUNT:
211  self._attr_native_value_attr_native_value = state.match_count
None __init__(self, HistoryStatsUpdateCoordinator coordinator, str name)
Definition: sensor.py:146
None __init__(self, HomeAssistant hass, HistoryStatsUpdateCoordinator coordinator, str sensor_type, str name, str|None unique_id, str source_entity_id)
Definition: sensor.py:180
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
float pretty_ratio(float value, tuple[datetime.datetime, datetime.datetime] period)
Definition: helpers.py:78
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: sensor.py:93
None async_setup_entry(HomeAssistant hass, HistoryStatsConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:120
dr.DeviceInfo|None async_device_info_to_link_from_entity(HomeAssistant hass, str entity_id_or_uuid)
Definition: device.py:28
None async_setup_reload_service(HomeAssistant hass, str domain, Iterable[str] platforms)
Definition: reload.py:191