277 lines
9.6 KiB
Python
277 lines
9.6 KiB
Python
"""Support for the Philips Hue sensors as a platform."""
|
|
import asyncio
|
|
from datetime import timedelta
|
|
import logging
|
|
from time import monotonic
|
|
|
|
from aiohue import AiohueException, Unauthorized
|
|
from aiohue.sensors import TYPE_ZLL_PRESENCE
|
|
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
|
|
|
|
from .helpers import remove_devices
|
|
|
|
CURRENT_SENSORS_FORMAT = "{}_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."""
|
|
sensor_key = CURRENT_SENSORS_FORMAT.format(config_entry.data["host"])
|
|
bridge = hass.data[hue.DOMAIN][config_entry.data["host"]]
|
|
hass.data[hue.DOMAIN].setdefault(sensor_key, {})
|
|
|
|
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, config_entry)
|
|
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, config_entry):
|
|
"""Initialize the sensor manager."""
|
|
self.hass = hass
|
|
self.bridge = bridge
|
|
self.config_entry = config_entry
|
|
self._component_add_entities = {}
|
|
self._started = False
|
|
|
|
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."""
|
|
|
|
# don't update when we are not authorized
|
|
if not self.bridge.authorized:
|
|
return
|
|
|
|
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."""
|
|
api = self.bridge.api.sensors
|
|
|
|
try:
|
|
start = monotonic()
|
|
with async_timeout.timeout(4):
|
|
await self.bridge.async_request_call(api.update())
|
|
except Unauthorized:
|
|
await self.bridge.handle_unauthorized_error()
|
|
return
|
|
except (asyncio.TimeoutError, 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 = {}
|
|
sensor_key = CURRENT_SENSORS_FORMAT.format(self.config_entry.data["host"])
|
|
current = self.hass.data[hue.DOMAIN][sensor_key]
|
|
|
|
# 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 != 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])
|
|
|
|
await remove_devices(
|
|
self.hass,
|
|
self.config_entry,
|
|
[value.uniqueid for value in api.values()],
|
|
current,
|
|
)
|
|
|
|
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.authorized
|
|
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}
|