Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Component providing HA sensor support for Ring Door Bell/Chimes."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 from typing import Any, Generic, cast
8 
9 from ring_doorbell import (
10  RingCapability,
11  RingChime,
12  RingDoorBell,
13  RingEventKind,
14  RingGeneric,
15  RingOther,
16 )
17 
19  SensorDeviceClass,
20  SensorEntity,
21  SensorEntityDescription,
22  SensorStateClass,
23 )
24 from homeassistant.const import (
25  PERCENTAGE,
26  SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
27  EntityCategory,
28  Platform,
29 )
30 from homeassistant.core import HomeAssistant, callback
31 from homeassistant.helpers.entity_platform import AddEntitiesCallback
32 from homeassistant.helpers.typing import StateType
33 
34 from . import RingConfigEntry
35 from .coordinator import RingDataCoordinator
36 from .entity import (
37  DeprecatedInfo,
38  RingDeviceT,
39  RingEntity,
40  RingEntityDescription,
41  async_check_create_deprecated,
42 )
43 
44 
46  hass: HomeAssistant,
47  entry: RingConfigEntry,
48  async_add_entities: AddEntitiesCallback,
49 ) -> None:
50  """Set up a sensor for a Ring device."""
51  ring_data = entry.runtime_data
52  devices_coordinator = ring_data.devices_coordinator
53 
54  entities = [
55  RingSensor(device, devices_coordinator, description)
56  for description in SENSOR_TYPES
57  for device in ring_data.devices.all_devices
58  if description.exists_fn(device)
60  hass,
61  Platform.SENSOR,
62  f"{device.id}-{description.key}",
63  description,
64  )
65  ]
66 
67  async_add_entities(entities)
68 
69 
70 class RingSensor(RingEntity[RingDeviceT], SensorEntity):
71  """A sensor implementation for Ring device."""
72 
73  entity_description: RingSensorEntityDescription[RingDeviceT]
74 
75  def __init__(
76  self,
77  device: RingDeviceT,
78  coordinator: RingDataCoordinator,
79  description: RingSensorEntityDescription[RingDeviceT],
80  ) -> None:
81  """Initialize a sensor for Ring device."""
82  super().__init__(device, coordinator)
83  self.entity_descriptionentity_description = description
84  self._attr_unique_id_attr_unique_id = f"{device.id}-{description.key}"
85  self._attr_entity_registry_enabled_default_attr_entity_registry_enabled_default = (
86  description.entity_registry_enabled_default
87  )
88  self._attr_native_value_attr_native_value = self.entity_descriptionentity_description.value_fn(self._device_device_device_device)
89 
90  @callback
91  def _handle_coordinator_update(self) -> None:
92  """Call update method."""
93 
94  self._device_device_device_device = cast(
95  RingDeviceT,
96  self._get_coordinator_data_get_coordinator_data().get_device(self._device_device_device_device.device_api_id),
97  )
98  # History values can drop off the last 10 events so only update
99  # the value if it's not None
100  if native_value := self.entity_descriptionentity_description.value_fn(self._device_device_device_device):
101  self._attr_native_value_attr_native_value = native_value
102  if extra_attrs := self.entity_descriptionentity_description.extra_state_attributes_fn(
103  self._device_device_device_device
104  ):
105  self._attr_extra_state_attributes_attr_extra_state_attributes_attr_extra_state_attributes = extra_attrs
107 
108 
110  history_data: list[dict[str, Any]], kind: RingEventKind | None
111 ) -> dict[str, Any] | None:
112  if not history_data:
113  return None
114  if kind is None:
115  return history_data[0]
116  for entry in history_data:
117  if entry["kind"] == kind.value:
118  return entry
119  return None
120 
121 
123  history_data: list[dict[str, Any]], kind: RingEventKind | None
124 ) -> dict[str, Any] | None:
125  if last_event := _get_last_event(history_data, kind):
126  return {
127  "created_at": last_event.get("created_at"),
128  "answered": last_event.get("answered"),
129  "recording_status": last_event.get("recording", {}).get("status"),
130  "category": last_event.get("kind"),
131  }
132  return None
133 
134 
135 @dataclass(frozen=True, kw_only=True)
137  SensorEntityDescription, RingEntityDescription, Generic[RingDeviceT]
138 ):
139  """Describes Ring sensor entity."""
140 
141  value_fn: Callable[[RingDeviceT], StateType] = lambda _: True
142  exists_fn: Callable[[RingGeneric], bool] = lambda _: True
143  extra_state_attributes_fn: Callable[[RingDeviceT], dict[str, Any] | None] = (
144  lambda _: None
145  )
146 
147 
148 # For some reason mypy doesn't properly type check the default TypeVar value here
149 # so for now the [RingGeneric] subscript needs to be specified.
150 # Once https://github.com/python/mypy/issues/14851 is closed this should hopefully
151 # be fixed and the [RingGeneric] subscript can be removed.
152 # https://github.com/home-assistant/core/pull/115276#discussion_r1560106576
153 SENSOR_TYPES: tuple[RingSensorEntityDescription[Any], ...] = (
154  RingSensorEntityDescription[RingGeneric](
155  key="battery",
156  native_unit_of_measurement=PERCENTAGE,
157  device_class=SensorDeviceClass.BATTERY,
158  state_class=SensorStateClass.MEASUREMENT,
159  entity_category=EntityCategory.DIAGNOSTIC,
160  value_fn=lambda device: device.battery_life,
161  exists_fn=lambda device: device.family != "chimes",
162  ),
163  RingSensorEntityDescription[RingGeneric](
164  key="last_activity",
165  translation_key="last_activity",
166  device_class=SensorDeviceClass.TIMESTAMP,
167  value_fn=lambda device: last_event.get("created_at")
168  if (last_event := _get_last_event(device.last_history, None))
169  else None,
170  extra_state_attributes_fn=lambda device: last_event_attrs
171  if (last_event_attrs := _get_last_event_attrs(device.last_history, None))
172  else None,
173  exists_fn=lambda device: device.has_capability(RingCapability.HISTORY),
174  ),
175  RingSensorEntityDescription[RingGeneric](
176  key="last_ding",
177  translation_key="last_ding",
178  device_class=SensorDeviceClass.TIMESTAMP,
179  value_fn=lambda device: last_event.get("created_at")
180  if (last_event := _get_last_event(device.last_history, RingEventKind.DING))
181  else None,
182  extra_state_attributes_fn=lambda device: last_event_attrs
183  if (
184  last_event_attrs := _get_last_event_attrs(
185  device.last_history, RingEventKind.DING
186  )
187  )
188  else None,
189  exists_fn=lambda device: device.has_capability(RingCapability.HISTORY),
190  deprecated_info=DeprecatedInfo(
191  new_platform=Platform.EVENT, breaks_in_ha_version="2025.4.0"
192  ),
193  ),
194  RingSensorEntityDescription[RingGeneric](
195  key="last_motion",
196  translation_key="last_motion",
197  device_class=SensorDeviceClass.TIMESTAMP,
198  value_fn=lambda device: last_event.get("created_at")
199  if (last_event := _get_last_event(device.last_history, RingEventKind.MOTION))
200  else None,
201  extra_state_attributes_fn=lambda device: last_event_attrs
202  if (
203  last_event_attrs := _get_last_event_attrs(
204  device.last_history, RingEventKind.MOTION
205  )
206  )
207  else None,
208  exists_fn=lambda device: device.has_capability(RingCapability.HISTORY),
209  deprecated_info=DeprecatedInfo(
210  new_platform=Platform.EVENT, breaks_in_ha_version="2025.4.0"
211  ),
212  ),
213  RingSensorEntityDescription[RingDoorBell | RingChime](
214  key="volume",
215  translation_key="volume",
216  value_fn=lambda device: device.volume,
217  exists_fn=lambda device: isinstance(device, (RingDoorBell, RingChime)),
218  deprecated_info=DeprecatedInfo(
219  new_platform=Platform.NUMBER, breaks_in_ha_version="2025.4.0"
220  ),
221  ),
222  RingSensorEntityDescription[RingOther](
223  key="doorbell_volume",
224  translation_key="doorbell_volume",
225  value_fn=lambda device: device.doorbell_volume,
226  exists_fn=lambda device: isinstance(device, RingOther),
227  deprecated_info=DeprecatedInfo(
228  new_platform=Platform.NUMBER, breaks_in_ha_version="2025.4.0"
229  ),
230  ),
231  RingSensorEntityDescription[RingOther](
232  key="mic_volume",
233  translation_key="mic_volume",
234  value_fn=lambda device: device.mic_volume,
235  exists_fn=lambda device: isinstance(device, RingOther),
236  deprecated_info=DeprecatedInfo(
237  new_platform=Platform.NUMBER, breaks_in_ha_version="2025.4.0"
238  ),
239  ),
240  RingSensorEntityDescription[RingOther](
241  key="voice_volume",
242  translation_key="voice_volume",
243  value_fn=lambda device: device.voice_volume,
244  exists_fn=lambda device: isinstance(device, RingOther),
245  deprecated_info=DeprecatedInfo(
246  new_platform=Platform.NUMBER, breaks_in_ha_version="2025.4.0"
247  ),
248  ),
249  RingSensorEntityDescription[RingGeneric](
250  key="wifi_signal_category",
251  translation_key="wifi_signal_category",
252  entity_category=EntityCategory.DIAGNOSTIC,
253  entity_registry_enabled_default=False,
254  value_fn=lambda device: device.wifi_signal_category,
255  ),
256  RingSensorEntityDescription[RingGeneric](
257  key="wifi_signal_strength",
258  translation_key="wifi_signal_strength",
259  native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
260  device_class=SensorDeviceClass.SIGNAL_STRENGTH,
261  entity_category=EntityCategory.DIAGNOSTIC,
262  entity_registry_enabled_default=False,
263  value_fn=lambda device: device.wifi_signal_strength,
264  ),
265 )
None __init__(self, RingDeviceT device, RingDataCoordinator coordinator, RingSensorEntityDescription[RingDeviceT] description)
Definition: sensor.py:80
DeviceEntry get_device(HomeAssistant hass, str unique_id)
Definition: util.py:12
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool async_check_create_deprecated(HomeAssistant hass, Platform platform, str unique_id, RingEntityDescription entity_description)
Definition: entity.py:97
dict[str, Any]|None _get_last_event_attrs(list[dict[str, Any]] history_data, RingEventKind|None kind)
Definition: sensor.py:124
dict[str, Any]|None _get_last_event(list[dict[str, Any]] history_data, RingEventKind|None kind)
Definition: sensor.py:111
None async_setup_entry(HomeAssistant hass, RingConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:49