Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for sensor data from RainMachine."""
2 
3 from __future__ import annotations
4 
5 from dataclasses import dataclass
6 from datetime import datetime, timedelta
7 from typing import Any, cast
8 
10  DOMAIN as SENSOR_DOMAIN,
11  RestoreSensor,
12  SensorDeviceClass,
13  SensorEntity,
14  SensorEntityDescription,
15  SensorStateClass,
16 )
17 from homeassistant.config_entries import ConfigEntry
18 from homeassistant.const import EntityCategory, UnitOfVolume
19 from homeassistant.core import HomeAssistant, callback
20 from homeassistant.helpers.entity_platform import AddEntitiesCallback
21 from homeassistant.util.dt import utc_from_timestamp, utcnow
22 
23 from . import RainMachineConfigEntry, RainMachineData
24 from .const import DATA_PROGRAMS, DATA_PROVISION_SETTINGS, DATA_ZONES
25 from .entity import RainMachineEntity, RainMachineEntityDescription
26 from .util import (
27  RUN_STATE_MAP,
28  EntityDomainReplacementStrategy,
29  RunStates,
30  async_finish_entity_domain_replacements,
31  key_exists,
32 )
33 
34 DEFAULT_ZONE_COMPLETION_TIME_WOBBLE_TOLERANCE = timedelta(seconds=5)
35 
36 TYPE_FLOW_SENSOR_CLICK_M3 = "flow_sensor_clicks_cubic_meter"
37 TYPE_FLOW_SENSOR_CONSUMED_LITERS = "flow_sensor_consumed_liters"
38 TYPE_FLOW_SENSOR_LEAK_CLICKS = "flow_sensor_leak_clicks"
39 TYPE_FLOW_SENSOR_LEAK_VOLUME = "flow_sensor_leak_volume"
40 TYPE_FLOW_SENSOR_START_INDEX = "flow_sensor_start_index"
41 TYPE_FLOW_SENSOR_WATERING_CLICKS = "flow_sensor_watering_clicks"
42 TYPE_LAST_LEAK_DETECTED = "last_leak_detected"
43 TYPE_PROGRAM_RUN_COMPLETION_TIME = "program_run_completion_time"
44 TYPE_RAIN_SENSOR_RAIN_START = "rain_sensor_rain_start"
45 TYPE_ZONE_RUN_COMPLETION_TIME = "zone_run_completion_time"
46 
47 
48 @dataclass(frozen=True, kw_only=True)
50  SensorEntityDescription, RainMachineEntityDescription
51 ):
52  """Describe a RainMachine sensor."""
53 
54  data_key: str
55 
56 
57 @dataclass(frozen=True, kw_only=True)
59  SensorEntityDescription, RainMachineEntityDescription
60 ):
61  """Describe a RainMachine completion timer sensor."""
62 
63  uid: int
64 
65 
66 SENSOR_DESCRIPTIONS = (
68  key=TYPE_FLOW_SENSOR_CLICK_M3,
69  translation_key=TYPE_FLOW_SENSOR_CLICK_M3,
70  native_unit_of_measurement=f"clicks/{UnitOfVolume.CUBIC_METERS}",
71  entity_category=EntityCategory.DIAGNOSTIC,
72  entity_registry_enabled_default=False,
73  state_class=SensorStateClass.MEASUREMENT,
74  api_category=DATA_PROVISION_SETTINGS,
75  data_key="flowSensorClicksPerCubicMeter",
76  ),
78  key=TYPE_FLOW_SENSOR_CONSUMED_LITERS,
79  translation_key=TYPE_FLOW_SENSOR_CONSUMED_LITERS,
80  device_class=SensorDeviceClass.WATER,
81  entity_category=EntityCategory.DIAGNOSTIC,
82  native_unit_of_measurement=UnitOfVolume.LITERS,
83  entity_registry_enabled_default=False,
84  state_class=SensorStateClass.TOTAL_INCREASING,
85  api_category=DATA_PROVISION_SETTINGS,
86  data_key="flowSensorWateringClicks",
87  ),
89  key=TYPE_FLOW_SENSOR_LEAK_CLICKS,
90  translation_key=TYPE_FLOW_SENSOR_LEAK_CLICKS,
91  entity_category=EntityCategory.DIAGNOSTIC,
92  native_unit_of_measurement="clicks",
93  entity_registry_enabled_default=False,
94  state_class=SensorStateClass.TOTAL_INCREASING,
95  api_category=DATA_PROVISION_SETTINGS,
96  data_key="flowSensorLeakClicks",
97  ),
99  key=TYPE_FLOW_SENSOR_LEAK_VOLUME,
100  translation_key=TYPE_FLOW_SENSOR_LEAK_VOLUME,
101  device_class=SensorDeviceClass.WATER,
102  entity_category=EntityCategory.DIAGNOSTIC,
103  native_unit_of_measurement=UnitOfVolume.LITERS,
104  entity_registry_enabled_default=False,
105  state_class=SensorStateClass.TOTAL_INCREASING,
106  api_category=DATA_PROVISION_SETTINGS,
107  data_key="flowSensorLeakClicks",
108  ),
110  key=TYPE_FLOW_SENSOR_START_INDEX,
111  translation_key=TYPE_FLOW_SENSOR_START_INDEX,
112  icon="mdi:water-pump",
113  entity_category=EntityCategory.DIAGNOSTIC,
114  native_unit_of_measurement="index",
115  entity_registry_enabled_default=False,
116  api_category=DATA_PROVISION_SETTINGS,
117  data_key="flowSensorStartIndex",
118  ),
120  key=TYPE_FLOW_SENSOR_WATERING_CLICKS,
121  translation_key=TYPE_FLOW_SENSOR_WATERING_CLICKS,
122  icon="mdi:water-pump",
123  entity_category=EntityCategory.DIAGNOSTIC,
124  native_unit_of_measurement="clicks",
125  entity_registry_enabled_default=False,
126  state_class=SensorStateClass.TOTAL_INCREASING,
127  api_category=DATA_PROVISION_SETTINGS,
128  data_key="flowSensorWateringClicks",
129  ),
131  key=TYPE_LAST_LEAK_DETECTED,
132  translation_key=TYPE_LAST_LEAK_DETECTED,
133  icon="mdi:pipe-leak",
134  entity_category=EntityCategory.DIAGNOSTIC,
135  entity_registry_enabled_default=False,
136  device_class=SensorDeviceClass.TIMESTAMP,
137  api_category=DATA_PROVISION_SETTINGS,
138  data_key="lastLeakDetected",
139  ),
141  key=TYPE_RAIN_SENSOR_RAIN_START,
142  translation_key=TYPE_RAIN_SENSOR_RAIN_START,
143  icon="mdi:weather-pouring",
144  entity_category=EntityCategory.DIAGNOSTIC,
145  entity_registry_enabled_default=False,
146  device_class=SensorDeviceClass.TIMESTAMP,
147  api_category=DATA_PROVISION_SETTINGS,
148  data_key="rainSensorRainStart",
149  ),
150 )
151 
152 
154  hass: HomeAssistant,
155  entry: RainMachineConfigEntry,
156  async_add_entities: AddEntitiesCallback,
157 ) -> None:
158  """Set up RainMachine sensors based on a config entry."""
159  data = entry.runtime_data
160 
162  hass,
163  entry,
164  (
166  SENSOR_DOMAIN,
167  f"{data.controller.mac}_freeze_protect_temp",
168  f"select.{data.controller.name.lower()}_freeze_protect_temperature",
169  breaks_in_ha_version="2022.12.0",
170  remove_old_entity=True,
171  ),
172  ),
173  )
174 
175  api_category_sensor_map = {
176  DATA_PROVISION_SETTINGS: ProvisionSettingsSensor,
177  }
178 
179  sensors: list[ProvisionSettingsSensor | TimeRemainingSensor] = [
180  api_category_sensor_map[description.api_category](entry, data, description)
181  for description in SENSOR_DESCRIPTIONS
182  if (
183  (coordinator := data.coordinators[description.api_category]) is not None
184  and coordinator.data
185  and key_exists(coordinator.data, description.data_key)
186  )
187  ]
188 
189  program_coordinator = data.coordinators[DATA_PROGRAMS]
190  zone_coordinator = data.coordinators[DATA_ZONES]
191 
192  for uid, program in program_coordinator.data.items():
193  sensors.append(
195  entry,
196  data,
198  key=f"{TYPE_PROGRAM_RUN_COMPLETION_TIME}_{uid}",
199  name=f"{program['name']} Run Completion Time",
200  device_class=SensorDeviceClass.TIMESTAMP,
201  entity_category=EntityCategory.DIAGNOSTIC,
202  api_category=DATA_PROGRAMS,
203  uid=uid,
204  ),
205  )
206  )
207 
208  for uid, zone in zone_coordinator.data.items():
209  sensors.append(
211  entry,
212  data,
214  key=f"{TYPE_ZONE_RUN_COMPLETION_TIME}_{uid}",
215  name=f"{zone['name']} Run Completion Time",
216  device_class=SensorDeviceClass.TIMESTAMP,
217  entity_category=EntityCategory.DIAGNOSTIC,
218  api_category=DATA_ZONES,
219  uid=uid,
220  ),
221  )
222  )
223 
224  async_add_entities(sensors)
225 
226 
228  """Define a sensor that shows the amount of time remaining for an activity."""
229 
230  entity_description: RainMachineSensorCompletionTimerDescription
231 
232  def __init__(
233  self,
234  entry: ConfigEntry,
235  data: RainMachineData,
236  description: RainMachineSensorCompletionTimerDescription,
237  ) -> None:
238  """Initialize."""
239  super().__init__(entry, data, description)
240 
241  self._current_run_state_current_run_state: RunStates | None = None
242  self._previous_run_state_previous_run_state: RunStates | None = None
243 
244  @property
245  def activity_data(self) -> dict[str, Any]:
246  """Return the core data for this entity."""
247  return cast(dict[str, Any], self.coordinator.data[self.entity_descriptionentity_description.uid])
248 
249  @property
250  def status_key(self) -> str:
251  """Return the data key that contains the activity status."""
252  return "state"
253 
254  async def async_added_to_hass(self) -> None:
255  """Handle entity which will be added."""
256  if restored_data := await self.async_get_last_sensor_dataasync_get_last_sensor_data():
257  self._attr_native_value_attr_native_value = restored_data.native_value
258  await super().async_added_to_hass()
259 
260  def calculate_seconds_remaining(self) -> int:
261  """Calculate the number of seconds remaining."""
262  raise NotImplementedError
263 
264  @callback
265  def update_from_latest_data(self) -> None:
266  """Update the state."""
267  self._previous_run_state_previous_run_state = self._current_run_state_current_run_state
268  self._current_run_state_current_run_state = RUN_STATE_MAP.get(self.activity_dataactivity_data[self.status_keystatus_key])
269 
270  now = utcnow()
271 
272  if (
273  self._current_run_state_current_run_state == RunStates.NOT_RUNNING
274  and self._previous_run_state_previous_run_state in (RunStates.QUEUED, RunStates.RUNNING)
275  ):
276  # If the activity goes from queued/running to not running, update the
277  # state to be right now (i.e., the time the zone stopped running):
278  self._attr_native_value_attr_native_value = now
279  elif self._current_run_state_current_run_state == RunStates.RUNNING:
280  seconds_remaining = self.calculate_seconds_remainingcalculate_seconds_remaining()
281  new_timestamp = now + timedelta(seconds=seconds_remaining)
282 
283  if (
284  isinstance(self._attr_native_value_attr_native_value, datetime)
285  and new_timestamp - self._attr_native_value_attr_native_value
286  < DEFAULT_ZONE_COMPLETION_TIME_WOBBLE_TOLERANCE
287  ):
288  # If the deviation between the previous and new timestamps is less
289  # than a "wobble tolerance," don't spam the state machine:
290  return
291 
292  self._attr_native_value_attr_native_value = new_timestamp
293 
294 
296  """Define a sensor that shows the amount of time remaining for a program."""
297 
298  @property
299  def status_key(self) -> str:
300  """Return the data key that contains the activity status."""
301  return "status"
302 
303  def calculate_seconds_remaining(self) -> int:
304  """Calculate the number of seconds remaining."""
305  return sum(
306  self._data_data.coordinators[DATA_ZONES].data[zone["id"]]["remaining"]
307  for zone in [z for z in self.activity_dataactivity_data["wateringTimes"] if z["active"]]
308  )
309 
310 
312  """Define a sensor that handles provisioning data."""
313 
314  entity_description: RainMachineSensorDataDescription
315 
316  @callback
317  def update_from_latest_data(self) -> None:
318  """Update the state."""
319  system = self.coordinator.data.get("system", {})
320  new_value = system.get(self.entity_descriptionentity_description.data_key)
321 
322  # Calculate volumetric sensors
323  if (
324  self.entity_descriptionentity_description.key
325  in {
326  TYPE_FLOW_SENSOR_CONSUMED_LITERS,
327  TYPE_FLOW_SENSOR_LEAK_VOLUME,
328  }
329  and new_value
330  ):
331  if clicks_per_m3 := system.get("flowSensorClicksPerCubicMeter"):
332  self._attr_native_value_attr_native_value = round((new_value * 1000) / clicks_per_m3, 1)
333  return
334 
335  # Convert timestamp sensors to datetime
336  if self.entity_descriptionentity_description.key in {
337  TYPE_LAST_LEAK_DETECTED,
338  TYPE_RAIN_SENSOR_RAIN_START,
339  }:
340  # Timestamp may return 0 instead of null, explicitly set to None
341  if new_value:
342  self._attr_native_value_attr_native_value = utc_from_timestamp(new_value)
343  else:
344  self._attr_native_value_attr_native_value = None
345  return
346 
347  # Return all other sensor values or None
348  self._attr_native_value_attr_native_value = new_value
349 
350 
352  """Define a sensor that shows the amount of time remaining for a zone."""
353 
354  def calculate_seconds_remaining(self) -> int:
355  """Calculate the number of seconds remaining."""
356  return cast(
357  int, self.coordinator.data[self.entity_descriptionentity_description.uid]["remaining"]
358  )
None __init__(self, ConfigEntry entry, RainMachineData data, RainMachineSensorCompletionTimerDescription description)
Definition: sensor.py:237
SensorExtraStoredData|None async_get_last_sensor_data(self)
Definition: __init__.py:934
None async_finish_entity_domain_replacements(HomeAssistant hass, ConfigEntry entry, Iterable[EntityDomainReplacementStrategy] entity_replacement_strategies)
Definition: util.py:41
None async_setup_entry(HomeAssistant hass, RainMachineConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:157
bool key_exists(dict[str, Any] data, str search_key)
Definition: util.py:70