Home Assistant Unofficial Reference 2024.12.1
sensor_base.py
Go to the documentation of this file.
1 """Support for the Philips Hue sensors as a platform."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from datetime import timedelta
7 import logging
8 from typing import Any
9 
10 from aiohue import AiohueException, Unauthorized
11 from aiohue.v1.sensors import TYPE_ZLL_PRESENCE
12 
13 from homeassistant.components.sensor import SensorStateClass
14 from homeassistant.core import callback
15 from homeassistant.helpers import debounce, entity
16 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
17 
18 from ..const import REQUEST_REFRESH_DELAY
19 from .helpers import remove_devices
20 from .hue_event import EVENT_CONFIG_MAP
21 from .sensor_device import GenericHueDevice
22 
23 SENSOR_CONFIG_MAP: dict[str, Any] = {}
24 LOGGER = logging.getLogger(__name__)
25 
26 
27 def _device_id(aiohue_sensor):
28  # Work out the shared device ID, as described below
29  device_id = aiohue_sensor.uniqueid
30  if device_id and len(device_id) > 23:
31  device_id = device_id[:23]
32  return device_id
33 
34 
36  """Class that handles registering and updating Hue sensor entities.
37 
38  Intended to be a singleton.
39  """
40 
41  SCAN_INTERVAL = timedelta(seconds=5)
42 
43  def __init__(self, bridge):
44  """Initialize the sensor manager."""
45  self.bridgebridge = bridge
46  self._component_add_entities_component_add_entities = {}
47  self.currentcurrent = {}
48  self.current_eventscurrent_events = {}
49 
50  self._enabled_platforms_enabled_platforms = ("binary_sensor", "sensor")
52  bridge.hass,
53  LOGGER,
54  name="sensor",
55  update_method=self.async_update_dataasync_update_data,
56  update_interval=self.SCAN_INTERVALSCAN_INTERVAL,
57  request_refresh_debouncer=debounce.Debouncer(
58  bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
59  ),
60  )
61 
62  async def async_update_data(self):
63  """Update sensor data."""
64  try:
65  async with asyncio.timeout(4):
66  return await self.bridgebridge.async_request_call(
67  self.bridgebridge.api.sensors.update
68  )
69  except Unauthorized as err:
70  await self.bridgebridge.handle_unauthorized_error()
71  raise UpdateFailed("Unauthorized") from err
72  except AiohueException as err:
73  raise UpdateFailed(f"Hue error: {err}") from err
74 
75  async def async_register_component(self, platform, async_add_entities):
76  """Register async_add_entities methods for components."""
77  self._component_add_entities_component_add_entities[platform] = async_add_entities
78 
79  if len(self._component_add_entities_component_add_entities) < len(self._enabled_platforms_enabled_platforms):
80  LOGGER.debug("Aborting start with %s, waiting for the rest", platform)
81  return
82 
83  # We have all components available, start the updating.
84  self.bridgebridge.reset_jobs.append(
85  self.coordinatorcoordinator.async_add_listener(self.async_update_itemsasync_update_items)
86  )
87  await self.coordinatorcoordinator.async_refresh()
88 
89  @callback
90  def async_update_items(self):
91  """Update sensors from the bridge."""
92  api = self.bridgebridge.api.sensors
93 
94  if len(self._component_add_entities_component_add_entities) < len(self._enabled_platforms_enabled_platforms):
95  return
96 
97  to_add = {}
98  primary_sensor_devices = {}
99  current = self.currentcurrent
100 
101  # Physical Hue motion sensors present as three sensors in the API: a
102  # presence sensor, a temperature sensor, and a light level sensor. Of
103  # these, only the presence sensor is assigned the user-friendly name
104  # that the user has given to the device. Each of these sensors is
105  # linked by a common device_id, which is the first twenty-three
106  # characters of the unique id (then followed by a hyphen and an ID
107  # specific to the individual sensor).
108  #
109  # To set up neat values, and assign the sensor entities to the same
110  # device, we first, iterate over all the sensors and find the Hue
111  # presence sensors, then iterate over all the remaining sensors -
112  # finding the remaining ones that may or may not be related to the
113  # presence sensors.
114  for item_id in api:
115  if api[item_id].type != TYPE_ZLL_PRESENCE:
116  continue
117 
118  primary_sensor_devices[_device_id(api[item_id])] = api[item_id]
119 
120  # Iterate again now we have all the presence sensors, and add the
121  # related sensors with nice names where appropriate.
122  for item_id in api:
123  uniqueid = api[item_id].uniqueid
124  if current.get(uniqueid, self.current_eventscurrent_events.get(uniqueid)) is not None:
125  continue
126 
127  sensor_type = api[item_id].type
128 
129  # Check for event generator devices
130  event_config = EVENT_CONFIG_MAP.get(sensor_type)
131  if event_config is not None:
132  base_name = api[item_id].name
133  name = event_config["name_format"].format(base_name)
134  new_event = event_config["class"](api[item_id], name, self.bridgebridge)
135  self.bridgebridge.hass.async_create_task(
136  new_event.async_update_device_registry()
137  )
138  self.current_eventscurrent_events[uniqueid] = new_event
139 
140  sensor_config = SENSOR_CONFIG_MAP.get(sensor_type)
141  if sensor_config is None:
142  continue
143 
144  base_name = api[item_id].name
145  primary_sensor = primary_sensor_devices.get(_device_id(api[item_id]))
146  if primary_sensor is not None:
147  base_name = primary_sensor.name
148  name = sensor_config["name_format"].format(base_name)
149 
150  current[uniqueid] = sensor_config["class"](
151  api[item_id], name, self.bridgebridge, primary_sensor=primary_sensor
152  )
153 
154  to_add.setdefault(sensor_config["platform"], []).append(current[uniqueid])
155 
156  self.bridgebridge.hass.async_create_task(
158  self.bridgebridge,
159  [value.uniqueid for value in api.values()],
160  current,
161  )
162  )
163 
164  for platform, value in to_add.items():
165  self._component_add_entities_component_add_entities[platform](value)
166 
167 
168 class GenericHueSensor(GenericHueDevice, entity.Entity): # pylint: disable=hass-enforce-class-module
169  """Representation of a Hue sensor."""
170 
171  should_poll = False
172 
173  @property
174  def available(self):
175  """Return if sensor is available."""
176  return self.bridgebridge.sensor_manager.coordinator.last_update_success and (
177  self.allow_unreachableallow_unreachable
178  # remotes like Hue Tap (ZGPSwitchSensor) have no _reachability_
179  or self.sensorsensor.config.get("reachable", True)
180  )
181 
182  @property
183  def state_class(self):
184  """Return the state class of this entity, from STATE_CLASSES, if any."""
185  return SensorStateClass.MEASUREMENT
186 
187  async def async_added_to_hass(self):
188  """When entity is added to hass."""
189  await super().async_added_to_hass()
190  self.async_on_remove(
191  self.bridgebridge.sensor_manager.coordinator.async_add_listener(
192  self.async_write_ha_state
193  )
194  )
195 
196  async def async_update(self):
197  """Update the entity.
198 
199  Only used by the generic entity update service.
200  """
201  await self.bridgebridge.sensor_manager.coordinator.async_request_refresh()
202 
203 
205  """Representation of a Hue-brand, physical sensor."""
206 
207  @property
209  """Return the device state attributes."""
210  return {"battery_level": self.sensorsensor.battery}
def async_register_component(self, platform, async_add_entities)
Definition: sensor_base.py:75
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
def remove_devices(bridge, api_ids, current)
Definition: helpers.py:8
None async_add_listener(HomeAssistant hass, Callable[[], None] listener)
Definition: __init__.py:82