Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Sensor platform for Ista EcoTrend integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from dataclasses import dataclass
7 import datetime
8 from enum import StrEnum
9 import logging
10 
12  StatisticData,
13  StatisticMetaData,
14 )
16  async_add_external_statistics,
17  get_instance,
18  get_last_statistics,
19 )
21  SensorDeviceClass,
22  SensorEntity,
23  SensorEntityDescription,
24  SensorStateClass,
25 )
26 from homeassistant.const import UnitOfEnergy, UnitOfVolume
27 from homeassistant.core import HomeAssistant
29  DeviceEntry,
30  DeviceEntryType,
31  DeviceInfo,
32 )
33 from homeassistant.helpers.entity_platform import AddEntitiesCallback
34 from homeassistant.helpers.typing import StateType
35 from homeassistant.helpers.update_coordinator import CoordinatorEntity
36 
37 from . import IstaConfigEntry
38 from .const import DOMAIN
39 from .coordinator import IstaCoordinator
40 from .util import IstaConsumptionType, IstaValueType, get_native_value, get_statistics
41 
42 _LOGGER = logging.getLogger(__name__)
43 
44 
45 @dataclass(kw_only=True, frozen=True)
47  """Ista EcoTrend Sensor Description."""
48 
49  consumption_type: IstaConsumptionType
50  value_type: IstaValueType | None = None
51 
52 
53 class IstaSensorEntity(StrEnum):
54  """Ista EcoTrend Entities."""
55 
56  HEATING = "heating"
57  HEATING_ENERGY = "heating_energy"
58  HEATING_COST = "heating_cost"
59 
60  HOT_WATER = "hot_water"
61  HOT_WATER_ENERGY = "hot_water_energy"
62  HOT_WATER_COST = "hot_water_cost"
63 
64  WATER = "water"
65  WATER_COST = "water_cost"
66 
67 
68 SENSOR_DESCRIPTIONS: tuple[IstaSensorEntityDescription, ...] = (
70  key=IstaSensorEntity.HEATING,
71  translation_key=IstaSensorEntity.HEATING,
72  suggested_display_precision=0,
73  consumption_type=IstaConsumptionType.HEATING,
74  native_unit_of_measurement="units",
75  state_class=SensorStateClass.TOTAL,
76  ),
78  key=IstaSensorEntity.HEATING_ENERGY,
79  translation_key=IstaSensorEntity.HEATING_ENERGY,
80  device_class=SensorDeviceClass.ENERGY,
81  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
82  state_class=SensorStateClass.TOTAL,
83  suggested_display_precision=1,
84  consumption_type=IstaConsumptionType.HEATING,
85  value_type=IstaValueType.ENERGY,
86  ),
88  key=IstaSensorEntity.HEATING_COST,
89  translation_key=IstaSensorEntity.HEATING_COST,
90  device_class=SensorDeviceClass.MONETARY,
91  native_unit_of_measurement="EUR",
92  state_class=SensorStateClass.TOTAL,
93  suggested_display_precision=0,
94  entity_registry_enabled_default=False,
95  consumption_type=IstaConsumptionType.HEATING,
96  value_type=IstaValueType.COSTS,
97  ),
99  key=IstaSensorEntity.HOT_WATER,
100  translation_key=IstaSensorEntity.HOT_WATER,
101  device_class=SensorDeviceClass.WATER,
102  native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
103  state_class=SensorStateClass.TOTAL,
104  suggested_display_precision=1,
105  consumption_type=IstaConsumptionType.HOT_WATER,
106  ),
108  key=IstaSensorEntity.HOT_WATER_ENERGY,
109  translation_key=IstaSensorEntity.HOT_WATER_ENERGY,
110  device_class=SensorDeviceClass.ENERGY,
111  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
112  state_class=SensorStateClass.TOTAL,
113  suggested_display_precision=1,
114  consumption_type=IstaConsumptionType.HOT_WATER,
115  value_type=IstaValueType.ENERGY,
116  ),
118  key=IstaSensorEntity.HOT_WATER_COST,
119  translation_key=IstaSensorEntity.HOT_WATER_COST,
120  device_class=SensorDeviceClass.MONETARY,
121  native_unit_of_measurement="EUR",
122  state_class=SensorStateClass.TOTAL,
123  suggested_display_precision=0,
124  entity_registry_enabled_default=False,
125  consumption_type=IstaConsumptionType.HOT_WATER,
126  value_type=IstaValueType.COSTS,
127  ),
129  key=IstaSensorEntity.WATER,
130  translation_key=IstaSensorEntity.WATER,
131  device_class=SensorDeviceClass.WATER,
132  native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
133  state_class=SensorStateClass.TOTAL,
134  suggested_display_precision=1,
135  entity_registry_enabled_default=False,
136  consumption_type=IstaConsumptionType.WATER,
137  ),
139  key=IstaSensorEntity.WATER_COST,
140  translation_key=IstaSensorEntity.WATER_COST,
141  device_class=SensorDeviceClass.MONETARY,
142  native_unit_of_measurement="EUR",
143  state_class=SensorStateClass.TOTAL,
144  suggested_display_precision=0,
145  entity_registry_enabled_default=False,
146  consumption_type=IstaConsumptionType.WATER,
147  value_type=IstaValueType.COSTS,
148  ),
149 )
150 
151 
153  hass: HomeAssistant,
154  config_entry: IstaConfigEntry,
155  async_add_entities: AddEntitiesCallback,
156 ) -> None:
157  """Set up the ista EcoTrend sensors."""
158 
159  coordinator = config_entry.runtime_data
160 
162  IstaSensor(coordinator, description, consumption_unit)
163  for description in SENSOR_DESCRIPTIONS
164  for consumption_unit in coordinator.data
165  )
166 
167 
168 class IstaSensor(CoordinatorEntity[IstaCoordinator], SensorEntity):
169  """Ista EcoTrend sensor."""
170 
171  entity_description: IstaSensorEntityDescription
172  _attr_has_entity_name = True
173  device_entry: DeviceEntry
174 
175  def __init__(
176  self,
177  coordinator: IstaCoordinator,
178  entity_description: IstaSensorEntityDescription,
179  consumption_unit: str,
180  ) -> None:
181  """Initialize the ista EcoTrend sensor."""
182  super().__init__(coordinator)
183  self.consumption_unitconsumption_unit = consumption_unit
184  self.entity_descriptionentity_description = entity_description
185  self._attr_unique_id_attr_unique_id = f"{consumption_unit}_{entity_description.key}"
186  self._attr_device_info_attr_device_info = DeviceInfo(
187  entry_type=DeviceEntryType.SERVICE,
188  manufacturer="ista SE",
189  model="ista EcoTrend",
190  name=f"{coordinator.details[consumption_unit]["address"]["street"]} "
191  f"{coordinator.details[consumption_unit]["address"]["houseNumber"]}".strip(),
192  configuration_url="https://ecotrend.ista.de/",
193  identifiers={(DOMAIN, consumption_unit)},
194  )
195 
196  @property
197  def native_value(self) -> StateType:
198  """Return the state of the device."""
199 
200  return get_native_value(
201  data=self.coordinator.data[self.consumption_unitconsumption_unit],
202  consumption_type=self.entity_descriptionentity_description.consumption_type,
203  value_type=self.entity_descriptionentity_description.value_type,
204  )
205 
206  async def async_added_to_hass(self) -> None:
207  """When added to hass."""
208  # perform initial statistics import when sensor is added, otherwise it would take
209  # 1 day when _handle_coordinator_update is triggered for the first time.
210  await self.update_statisticsupdate_statistics()
211  await super().async_added_to_hass()
212 
213  def _handle_coordinator_update(self) -> None:
214  """Handle coordinator update."""
215  asyncio.run_coroutine_threadsafe(self.update_statisticsupdate_statistics(), self.hasshasshass.loop)
216 
217  async def update_statistics(self) -> None:
218  """Import ista EcoTrend historical statistics."""
219 
220  # Remember the statistic_id that was initially created
221  # in case the entity gets renamed, because we cannot
222  # change the statistic_id
223  name = self.coordinator.config_entry.options.get(
224  f"lts_{self.entity_description.key}_{self.consumption_unit}"
225  )
226  if not name:
227  name = self.entity_identity_id.removeprefix("sensor.")
228  self.hasshasshass.config_entries.async_update_entry(
229  entry=self.coordinator.config_entry,
230  options={
231  **self.coordinator.config_entry.options,
232  f"lts_{self.entity_description.key}_{self.consumption_unit}": name,
233  },
234  )
235 
236  statistic_id = f"{DOMAIN}:{name}"
237  statistics_sum = 0.0
238  statistics_since = None
239 
240  last_stats = await get_instance(self.hasshasshass).async_add_executor_job(
241  get_last_statistics,
242  self.hasshasshass,
243  1,
244  statistic_id,
245  False,
246  {"sum"},
247  )
248 
249  _LOGGER.debug("Last statistics: %s", last_stats)
250 
251  if last_stats:
252  statistics_sum = last_stats[statistic_id][0].get("sum") or 0.0
253  statistics_since = datetime.datetime.fromtimestamp(
254  last_stats[statistic_id][0].get("end") or 0, tz=datetime.UTC
255  ) + datetime.timedelta(days=1)
256 
257  if monthly_consumptions := get_statistics(
258  self.coordinator.data[self.consumption_unitconsumption_unit],
259  self.entity_descriptionentity_description.consumption_type,
260  self.entity_descriptionentity_description.value_type,
261  ):
262  statistics: list[StatisticData] = [
263  {
264  "start": consumptions["date"],
265  "state": consumptions["value"],
266  "sum": (statistics_sum := statistics_sum + consumptions["value"]),
267  }
268  for consumptions in monthly_consumptions
269  if statistics_since is None or consumptions["date"] > statistics_since
270  ]
271 
272  metadata: StatisticMetaData = {
273  "has_mean": False,
274  "has_sum": True,
275  "name": f"{self.device_entry.name} {self.name}",
276  "source": DOMAIN,
277  "statistic_id": statistic_id,
278  "unit_of_measurement": self.entity_descriptionentity_description.native_unit_of_measurement,
279  }
280  if statistics:
281  _LOGGER.debug("Insert statistics: %s %s", metadata, statistics)
282  async_add_external_statistics(self.hasshasshass, metadata, statistics)
None __init__(self, IstaCoordinator coordinator, IstaSensorEntityDescription entity_description, str consumption_unit)
Definition: sensor.py:180
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_entry(HomeAssistant hass, IstaConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:156
list[dict[str, Any]]|None get_statistics(data, IstaConsumptionType consumption_type, IstaValueType|None value_type=None)
Definition: util.py:105
int|float|None get_native_value(data, IstaConsumptionType consumption_type, IstaValueType|None value_type=None)
Definition: util.py:93
None async_add_external_statistics(HomeAssistant hass, StatisticMetaData metadata, Iterable[StatisticData] statistics)
Definition: statistics.py:2318
Recorder get_instance(HomeAssistant hass)
Definition: recorder.py:74