forked from home-assistant/core
/
sensor_base.py
284 lines (226 loc) · 9.78 KB
/
sensor_base.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
"""Support for the Philips Hue sensors as a platform."""
import asyncio
from datetime import timedelta
import logging
from time import monotonic
import async_timeout
from homeassistant.components import hue
from homeassistant.exceptions import NoEntitySpecifiedError
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
CURRENT_SENSORS = 'current_sensors'
SENSOR_MANAGER_FORMAT = '{}_sensor_manager'
_LOGGER = logging.getLogger(__name__)
def _device_id(aiohue_sensor):
# Work out the shared device ID, as described below
device_id = aiohue_sensor.uniqueid
if device_id and len(device_id) > 23:
device_id = device_id[:23]
return device_id
async def async_setup_entry(hass, config_entry, async_add_entities,
binary=False):
"""Set up the Hue sensors from a config entry."""
bridge = hass.data[hue.DOMAIN][config_entry.data['host']]
hass.data[hue.DOMAIN].setdefault(CURRENT_SENSORS, {})
sm_key = SENSOR_MANAGER_FORMAT.format(config_entry.data['host'])
manager = hass.data[hue.DOMAIN].get(sm_key)
if manager is None:
manager = SensorManager(hass, bridge)
hass.data[hue.DOMAIN][sm_key] = manager
manager.register_component(binary, async_add_entities)
await manager.start()
class SensorManager:
"""Class that handles registering and updating Hue sensor entities.
Intended to be a singleton.
"""
SCAN_INTERVAL = timedelta(seconds=5)
sensor_config_map = {}
def __init__(self, hass, bridge):
"""Initialize the sensor manager."""
import aiohue
from .binary_sensor import HuePresence, PRESENCE_NAME_FORMAT
from .sensor import (
HueLightLevel, HueTemperature, LIGHT_LEVEL_NAME_FORMAT,
TEMPERATURE_NAME_FORMAT)
self.hass = hass
self.bridge = bridge
self._component_add_entities = {}
self._started = False
self.sensor_config_map.update({
aiohue.sensors.TYPE_ZLL_LIGHTLEVEL: {
"binary": False,
"name_format": LIGHT_LEVEL_NAME_FORMAT,
"class": HueLightLevel,
},
aiohue.sensors.TYPE_ZLL_TEMPERATURE: {
"binary": False,
"name_format": TEMPERATURE_NAME_FORMAT,
"class": HueTemperature,
},
aiohue.sensors.TYPE_ZLL_PRESENCE: {
"binary": True,
"name_format": PRESENCE_NAME_FORMAT,
"class": HuePresence,
},
})
def register_component(self, binary, async_add_entities):
"""Register async_add_entities methods for components."""
self._component_add_entities[binary] = async_add_entities
async def start(self):
"""Start updating sensors from the bridge on a schedule."""
# but only if it's not already started, and when we've got both
# async_add_entities methods
if self._started or len(self._component_add_entities) < 2:
return
self._started = True
_LOGGER.info('Starting sensor polling loop with %s second interval',
self.SCAN_INTERVAL.total_seconds())
async def async_update_bridge(now):
"""Will update sensors from the bridge."""
await self.async_update_items()
async_track_point_in_utc_time(
self.hass, async_update_bridge, utcnow() + self.SCAN_INTERVAL)
await async_update_bridge(None)
async def async_update_items(self):
"""Update sensors from the bridge."""
import aiohue
api = self.bridge.api.sensors
try:
start = monotonic()
with async_timeout.timeout(4):
await api.update()
except (asyncio.TimeoutError, aiohue.AiohueException) as err:
_LOGGER.debug('Failed to fetch sensor: %s', err)
if not self.bridge.available:
return
_LOGGER.error('Unable to reach bridge %s (%s)', self.bridge.host,
err)
self.bridge.available = False
return
finally:
_LOGGER.debug('Finished sensor request in %.3f seconds',
monotonic() - start)
if not self.bridge.available:
_LOGGER.info('Reconnected to bridge %s', self.bridge.host)
self.bridge.available = True
new_sensors = []
new_binary_sensors = []
primary_sensor_devices = {}
current = self.hass.data[hue.DOMAIN][CURRENT_SENSORS]
# Physical Hue motion sensors present as three sensors in the API: a
# presence sensor, a temperature sensor, and a light level sensor. Of
# these, only the presence sensor is assigned the user-friendly name
# that the user has given to the device. Each of these sensors is
# linked by a common device_id, which is the first twenty-three
# characters of the unique id (then followed by a hyphen and an ID
# specific to the individual sensor).
#
# To set up neat values, and assign the sensor entities to the same
# device, we first, iterate over all the sensors and find the Hue
# presence sensors, then iterate over all the remaining sensors -
# finding the remaining ones that may or may not be related to the
# presence sensors.
for item_id in api:
if api[item_id].type != aiohue.sensors.TYPE_ZLL_PRESENCE:
continue
primary_sensor_devices[_device_id(api[item_id])] = api[item_id]
# Iterate again now we have all the presence sensors, and add the
# related sensors with nice names where appropriate.
for item_id in api:
existing = current.get(api[item_id].uniqueid)
if existing is not None:
self.hass.async_create_task(
existing.async_maybe_update_ha_state())
continue
primary_sensor = None
sensor_config = self.sensor_config_map.get(api[item_id].type)
if sensor_config is None:
continue
base_name = api[item_id].name
primary_sensor = primary_sensor_devices.get(
_device_id(api[item_id]))
if primary_sensor is not None:
base_name = primary_sensor.name
name = sensor_config["name_format"].format(base_name)
current[api[item_id].uniqueid] = sensor_config["class"](
api[item_id], name, self.bridge, primary_sensor=primary_sensor)
if sensor_config['binary']:
new_binary_sensors.append(current[api[item_id].uniqueid])
else:
new_sensors.append(current[api[item_id].uniqueid])
async_add_sensor_entities = self._component_add_entities.get(False)
async_add_binary_entities = self._component_add_entities.get(True)
if new_sensors and async_add_sensor_entities:
async_add_sensor_entities(new_sensors)
if new_binary_sensors and async_add_binary_entities:
async_add_binary_entities(new_binary_sensors)
class GenericHueSensor:
"""Representation of a Hue sensor."""
should_poll = False
def __init__(self, sensor, name, bridge, primary_sensor=None):
"""Initialize the sensor."""
self.sensor = sensor
self._name = name
self._primary_sensor = primary_sensor
self.bridge = bridge
async def _async_update_ha_state(self, *args, **kwargs):
raise NotImplementedError
@property
def primary_sensor(self):
"""Return the primary sensor entity of the physical device."""
return self._primary_sensor or self.sensor
@property
def device_id(self):
"""Return the ID of the physical device this sensor is part of."""
return self.unique_id[:23]
@property
def unique_id(self):
"""Return the ID of this Hue sensor."""
return self.sensor.uniqueid
@property
def name(self):
"""Return a friendly name for the sensor."""
return self._name
@property
def available(self):
"""Return if sensor is available."""
return self.bridge.available and (self.bridge.allow_unreachable or
self.sensor.config['reachable'])
@property
def swupdatestate(self):
"""Return detail of available software updates for this device."""
return self.primary_sensor.raw.get('swupdate', {}).get('state')
async def async_maybe_update_ha_state(self):
"""Try to update Home Assistant with current state of entity.
But if it's not been added to hass yet, then don't throw an error.
"""
try:
await self._async_update_ha_state()
except (RuntimeError, NoEntitySpecifiedError):
_LOGGER.debug(
"Hue sensor update requested before it has been added.")
@property
def device_info(self):
"""Return the device info.
Links individual entities together in the hass device registry.
"""
return {
'identifiers': {
(hue.DOMAIN, self.device_id)
},
'name': self.primary_sensor.name,
'manufacturer': self.primary_sensor.manufacturername,
'model': (
self.primary_sensor.productname or
self.primary_sensor.modelid),
'sw_version': self.primary_sensor.swversion,
'via_device': (hue.DOMAIN, self.bridge.api.config.bridgeid),
}
class GenericZLLSensor(GenericHueSensor):
"""Representation of a Hue-brand, physical sensor."""
@property
def device_state_attributes(self):
"""Return the device state attributes."""
return {
"battery_level": self.sensor.battery
}