mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Add basic support for native Hue sensors (#22598)
* Add basic support for native Hue sensors * Update coveragerc * Simplify attributes * Remove config option * Refactor and document device-ness and update mechanism * Entity docstrings * Remove lingering config for sensors * Whitespace * Remove redundant entity ID generation and hass assignment. * More meaningful variable name. * Add new 'not-darkness' pseudo-sensor. * Refactor sensors into separate binary, non-binary, and shared modules. * formatting * make linter happy. * Refactor again, fix update mechanism, and address comments. * Remove unnecessary assignment * Small fixes. * docstring * Another refactor: only call API once and make testing easier * Tests & test fixes * Flake & lint * Use gather and dispatcher * Remove unnecessary whitespace change. * Move component related stuff out of the shared module * Remove unused remnant of failed approach. * Increase test coverage * Don't get too upset if we're already trying to update an entity before it has finished adding * relative imports
This commit is contained in:
parent
77244eab1e
commit
474ac8b09e
27
homeassistant/components/hue/binary_sensor.py
Normal file
27
homeassistant/components/hue/binary_sensor.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
"""Hue binary sensor entities."""
|
||||||
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
|
from homeassistant.components.hue.sensor_base import (
|
||||||
|
GenericZLLSensor, async_setup_entry as shared_async_setup_entry)
|
||||||
|
|
||||||
|
|
||||||
|
PRESENCE_NAME_FORMAT = "{} presence"
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Defer binary sensor setup to the shared sensor module."""
|
||||||
|
await shared_async_setup_entry(
|
||||||
|
hass, config_entry, async_add_entities, binary=True)
|
||||||
|
|
||||||
|
|
||||||
|
class HuePresence(GenericZLLSensor, BinarySensorDevice):
|
||||||
|
"""The presence sensor entity for a Hue motion sensor device."""
|
||||||
|
|
||||||
|
device_class = 'presence'
|
||||||
|
|
||||||
|
async def _async_update_ha_state(self, *args, **kwargs):
|
||||||
|
await self.async_update_ha_state(self, *args, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if the binary sensor is on."""
|
||||||
|
return self.sensor.presence
|
@ -69,6 +69,10 @@ class HueBridge:
|
|||||||
|
|
||||||
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
||||||
self.config_entry, 'light'))
|
self.config_entry, 'light'))
|
||||||
|
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
||||||
|
self.config_entry, 'binary_sensor'))
|
||||||
|
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
||||||
|
self.config_entry, 'sensor'))
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene,
|
DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene,
|
||||||
@ -94,8 +98,16 @@ class HueBridge:
|
|||||||
|
|
||||||
# If setup was successful, we set api variable, forwarded entry and
|
# If setup was successful, we set api variable, forwarded entry and
|
||||||
# register service
|
# register service
|
||||||
return await self.hass.config_entries.async_forward_entry_unload(
|
results = await asyncio.gather(
|
||||||
self.config_entry, 'light')
|
self.hass.config_entries.async_forward_entry_unload(
|
||||||
|
self.config_entry, 'light'),
|
||||||
|
self.hass.config_entries.async_forward_entry_unload(
|
||||||
|
self.config_entry, 'binary_sensor'),
|
||||||
|
self.hass.config_entries.async_forward_entry_unload(
|
||||||
|
self.config_entry, 'sensor')
|
||||||
|
)
|
||||||
|
# None and True are OK
|
||||||
|
return False not in results
|
||||||
|
|
||||||
async def hue_activate_scene(self, call, updated=False):
|
async def hue_activate_scene(self, call, updated=False):
|
||||||
"""Service to call directly into bridge to set scenes."""
|
"""Service to call directly into bridge to set scenes."""
|
||||||
|
57
homeassistant/components/hue/sensor.py
Normal file
57
homeassistant/components/hue/sensor.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
"""Hue sensor entities."""
|
||||||
|
from homeassistant.const import (
|
||||||
|
DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS)
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.components.hue.sensor_base import (
|
||||||
|
GenericZLLSensor, async_setup_entry as shared_async_setup_entry)
|
||||||
|
|
||||||
|
|
||||||
|
LIGHT_LEVEL_NAME_FORMAT = "{} light level"
|
||||||
|
TEMPERATURE_NAME_FORMAT = "{} temperature"
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Defer sensor setup to the shared sensor module."""
|
||||||
|
await shared_async_setup_entry(
|
||||||
|
hass, config_entry, async_add_entities, binary=False)
|
||||||
|
|
||||||
|
|
||||||
|
class GenericHueGaugeSensorEntity(GenericZLLSensor, Entity):
|
||||||
|
"""Parent class for all 'gauge' Hue device sensors."""
|
||||||
|
|
||||||
|
async def _async_update_ha_state(self, *args, **kwargs):
|
||||||
|
await self.async_update_ha_state(self, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class HueLightLevel(GenericHueGaugeSensorEntity):
|
||||||
|
"""The light level sensor entity for a Hue motion sensor device."""
|
||||||
|
|
||||||
|
device_class = DEVICE_CLASS_ILLUMINANCE
|
||||||
|
unit_of_measurement = "Lux"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the device."""
|
||||||
|
return self.sensor.lightlevel
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the device state attributes."""
|
||||||
|
attributes = super().device_state_attributes
|
||||||
|
attributes.update({
|
||||||
|
"threshold_dark": self.sensor.tholddark,
|
||||||
|
"threshold_offset": self.sensor.tholdoffset,
|
||||||
|
})
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
|
class HueTemperature(GenericHueGaugeSensorEntity):
|
||||||
|
"""The temperature sensor entity for a Hue motion sensor device."""
|
||||||
|
|
||||||
|
device_class = DEVICE_CLASS_TEMPERATURE
|
||||||
|
unit_of_measurement = TEMP_CELSIUS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the device."""
|
||||||
|
return self.sensor.temperature / 100
|
283
homeassistant/components/hue/sensor_base.py
Normal file
283
homeassistant/components/hue/sensor_base.py
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
"""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 = '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, {})
|
||||||
|
|
||||||
|
manager = hass.data[hue.DOMAIN].get(SENSOR_MANAGER)
|
||||||
|
if manager is None:
|
||||||
|
manager = SensorManager(hass, bridge)
|
||||||
|
hass.data[hue.DOMAIN][SENSOR_MANAGER] = 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_hub': (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
|
||||||
|
}
|
@ -21,9 +21,13 @@ async def test_bridge_setup():
|
|||||||
assert await hue_bridge.async_setup() is True
|
assert await hue_bridge.async_setup() is True
|
||||||
|
|
||||||
assert hue_bridge.api is api
|
assert hue_bridge.api is api
|
||||||
assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 1
|
forward_entries = set(
|
||||||
assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == \
|
c[1][1]
|
||||||
(entry, 'light')
|
for c in
|
||||||
|
hass.config_entries.async_forward_entry_setup.mock_calls
|
||||||
|
)
|
||||||
|
assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 3
|
||||||
|
assert forward_entries == set(['light', 'binary_sensor', 'sensor'])
|
||||||
|
|
||||||
|
|
||||||
async def test_bridge_setup_invalid_username():
|
async def test_bridge_setup_invalid_username():
|
||||||
@ -84,11 +88,11 @@ async def test_reset_unloads_entry_if_setup():
|
|||||||
assert await hue_bridge.async_setup() is True
|
assert await hue_bridge.async_setup() is True
|
||||||
|
|
||||||
assert len(hass.services.async_register.mock_calls) == 1
|
assert len(hass.services.async_register.mock_calls) == 1
|
||||||
assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 1
|
assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 3
|
||||||
|
|
||||||
hass.config_entries.async_forward_entry_unload.return_value = \
|
hass.config_entries.async_forward_entry_unload.return_value = \
|
||||||
mock_coro(True)
|
mock_coro(True)
|
||||||
assert await hue_bridge.async_reset()
|
assert await hue_bridge.async_reset()
|
||||||
|
|
||||||
assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 1
|
assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 3
|
||||||
assert len(hass.services.async_remove.mock_calls) == 1
|
assert len(hass.services.async_remove.mock_calls) == 1
|
||||||
|
485
tests/components/hue/test_sensor_base.py
Normal file
485
tests/components/hue/test_sensor_base.py
Normal file
@ -0,0 +1,485 @@
|
|||||||
|
"""Philips Hue sensors platform tests."""
|
||||||
|
import asyncio
|
||||||
|
from collections import deque
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import aiohue
|
||||||
|
from aiohue.sensors import Sensors
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components import hue
|
||||||
|
from homeassistant.components.hue import sensor_base as hue_sensor_base
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PRESENCE_SENSOR_1_PRESENT = {
|
||||||
|
"state": {
|
||||||
|
"presence": True,
|
||||||
|
"lastupdated": "2019-01-01T01:00:00"
|
||||||
|
},
|
||||||
|
"swupdate": {
|
||||||
|
"state": "noupdates",
|
||||||
|
"lastinstall": "2019-01-01T00:00:00"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"on": True,
|
||||||
|
"battery": 100,
|
||||||
|
"reachable": True,
|
||||||
|
"alert": "none",
|
||||||
|
"ledindication": False,
|
||||||
|
"usertest": False,
|
||||||
|
"sensitivity": 2,
|
||||||
|
"sensitivitymax": 2,
|
||||||
|
"pending": []
|
||||||
|
},
|
||||||
|
"name": "Living room sensor",
|
||||||
|
"type": "ZLLPresence",
|
||||||
|
"modelid": "SML001",
|
||||||
|
"manufacturername": "Philips",
|
||||||
|
"productname": "Hue motion sensor",
|
||||||
|
"swversion": "6.1.1.27575",
|
||||||
|
"uniqueid": "00:11:22:33:44:55:66:77-02-0406",
|
||||||
|
"capabilities": {
|
||||||
|
"certified": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LIGHT_LEVEL_SENSOR_1 = {
|
||||||
|
"state": {
|
||||||
|
"lightlevel": 0,
|
||||||
|
"dark": True,
|
||||||
|
"daylight": True,
|
||||||
|
"lastupdated": "2019-01-01T01:00:00"
|
||||||
|
},
|
||||||
|
"swupdate": {
|
||||||
|
"state": "noupdates",
|
||||||
|
"lastinstall": "2019-01-01T00:00:00"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"on": True,
|
||||||
|
"battery": 100,
|
||||||
|
"reachable": True,
|
||||||
|
"alert": "none",
|
||||||
|
"tholddark": 12467,
|
||||||
|
"tholdoffset": 7000,
|
||||||
|
"ledindication": False,
|
||||||
|
"usertest": False,
|
||||||
|
"pending": []
|
||||||
|
},
|
||||||
|
"name": "Hue ambient light sensor 1",
|
||||||
|
"type": "ZLLLightLevel",
|
||||||
|
"modelid": "SML001",
|
||||||
|
"manufacturername": "Philips",
|
||||||
|
"productname": "Hue ambient light sensor",
|
||||||
|
"swversion": "6.1.1.27575",
|
||||||
|
"uniqueid": "00:11:22:33:44:55:66:77-02-0400",
|
||||||
|
"capabilities": {
|
||||||
|
"certified": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TEMPERATURE_SENSOR_1 = {
|
||||||
|
"state": {
|
||||||
|
"temperature": 1775,
|
||||||
|
"lastupdated": "2019-01-01T01:00:00"
|
||||||
|
},
|
||||||
|
"swupdate": {
|
||||||
|
"state": "noupdates",
|
||||||
|
"lastinstall": "2019-01-01T01:00:00"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"on": True,
|
||||||
|
"battery": 100,
|
||||||
|
"reachable": True,
|
||||||
|
"alert": "none",
|
||||||
|
"ledindication": False,
|
||||||
|
"usertest": False,
|
||||||
|
"pending": []
|
||||||
|
},
|
||||||
|
"name": "Hue temperature sensor 1",
|
||||||
|
"type": "ZLLTemperature",
|
||||||
|
"modelid": "SML001",
|
||||||
|
"manufacturername": "Philips",
|
||||||
|
"productname": "Hue temperature sensor",
|
||||||
|
"swversion": "6.1.1.27575",
|
||||||
|
"uniqueid": "00:11:22:33:44:55:66:77-02-0402",
|
||||||
|
"capabilities": {
|
||||||
|
"certified": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PRESENCE_SENSOR_2_NOT_PRESENT = {
|
||||||
|
"state": {
|
||||||
|
"presence": False,
|
||||||
|
"lastupdated": "2019-01-01T00:00:00"
|
||||||
|
},
|
||||||
|
"swupdate": {
|
||||||
|
"state": "noupdates",
|
||||||
|
"lastinstall": "2019-01-01T01:00:00"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"on": True,
|
||||||
|
"battery": 100,
|
||||||
|
"reachable": True,
|
||||||
|
"alert": "none",
|
||||||
|
"ledindication": False,
|
||||||
|
"usertest": False,
|
||||||
|
"sensitivity": 2,
|
||||||
|
"sensitivitymax": 2,
|
||||||
|
"pending": []
|
||||||
|
},
|
||||||
|
"name": "Kitchen sensor",
|
||||||
|
"type": "ZLLPresence",
|
||||||
|
"modelid": "SML001",
|
||||||
|
"manufacturername": "Philips",
|
||||||
|
"productname": "Hue motion sensor",
|
||||||
|
"swversion": "6.1.1.27575",
|
||||||
|
"uniqueid": "00:11:22:33:44:55:66:88-02-0406",
|
||||||
|
"capabilities": {
|
||||||
|
"certified": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LIGHT_LEVEL_SENSOR_2 = {
|
||||||
|
"state": {
|
||||||
|
"lightlevel": 100,
|
||||||
|
"dark": True,
|
||||||
|
"daylight": True,
|
||||||
|
"lastupdated": "2019-01-01T01:00:00"
|
||||||
|
},
|
||||||
|
"swupdate": {
|
||||||
|
"state": "noupdates",
|
||||||
|
"lastinstall": "2019-01-01T00:00:00"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"on": True,
|
||||||
|
"battery": 100,
|
||||||
|
"reachable": True,
|
||||||
|
"alert": "none",
|
||||||
|
"tholddark": 12467,
|
||||||
|
"tholdoffset": 7000,
|
||||||
|
"ledindication": False,
|
||||||
|
"usertest": False,
|
||||||
|
"pending": []
|
||||||
|
},
|
||||||
|
"name": "Hue ambient light sensor 2",
|
||||||
|
"type": "ZLLLightLevel",
|
||||||
|
"modelid": "SML001",
|
||||||
|
"manufacturername": "Philips",
|
||||||
|
"productname": "Hue ambient light sensor",
|
||||||
|
"swversion": "6.1.1.27575",
|
||||||
|
"uniqueid": "00:11:22:33:44:55:66:88-02-0400",
|
||||||
|
"capabilities": {
|
||||||
|
"certified": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TEMPERATURE_SENSOR_2 = {
|
||||||
|
"state": {
|
||||||
|
"temperature": 1875,
|
||||||
|
"lastupdated": "2019-01-01T01:00:00"
|
||||||
|
},
|
||||||
|
"swupdate": {
|
||||||
|
"state": "noupdates",
|
||||||
|
"lastinstall": "2019-01-01T01:00:00"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"on": True,
|
||||||
|
"battery": 100,
|
||||||
|
"reachable": True,
|
||||||
|
"alert": "none",
|
||||||
|
"ledindication": False,
|
||||||
|
"usertest": False,
|
||||||
|
"pending": []
|
||||||
|
},
|
||||||
|
"name": "Hue temperature sensor 2",
|
||||||
|
"type": "ZLLTemperature",
|
||||||
|
"modelid": "SML001",
|
||||||
|
"manufacturername": "Philips",
|
||||||
|
"productname": "Hue temperature sensor",
|
||||||
|
"swversion": "6.1.1.27575",
|
||||||
|
"uniqueid": "00:11:22:33:44:55:66:88-02-0402",
|
||||||
|
"capabilities": {
|
||||||
|
"certified": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PRESENCE_SENSOR_3_PRESENT = {
|
||||||
|
"state": {
|
||||||
|
"presence": True,
|
||||||
|
"lastupdated": "2019-01-01T01:00:00"
|
||||||
|
},
|
||||||
|
"swupdate": {
|
||||||
|
"state": "noupdates",
|
||||||
|
"lastinstall": "2019-01-01T00:00:00"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"on": True,
|
||||||
|
"battery": 100,
|
||||||
|
"reachable": True,
|
||||||
|
"alert": "none",
|
||||||
|
"ledindication": False,
|
||||||
|
"usertest": False,
|
||||||
|
"sensitivity": 2,
|
||||||
|
"sensitivitymax": 2,
|
||||||
|
"pending": []
|
||||||
|
},
|
||||||
|
"name": "Bedroom sensor",
|
||||||
|
"type": "ZLLPresence",
|
||||||
|
"modelid": "SML001",
|
||||||
|
"manufacturername": "Philips",
|
||||||
|
"productname": "Hue motion sensor",
|
||||||
|
"swversion": "6.1.1.27575",
|
||||||
|
"uniqueid": "00:11:22:33:44:55:66:99-02-0406",
|
||||||
|
"capabilities": {
|
||||||
|
"certified": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LIGHT_LEVEL_SENSOR_3 = {
|
||||||
|
"state": {
|
||||||
|
"lightlevel": 0,
|
||||||
|
"dark": True,
|
||||||
|
"daylight": True,
|
||||||
|
"lastupdated": "2019-01-01T01:00:00"
|
||||||
|
},
|
||||||
|
"swupdate": {
|
||||||
|
"state": "noupdates",
|
||||||
|
"lastinstall": "2019-01-01T00:00:00"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"on": True,
|
||||||
|
"battery": 100,
|
||||||
|
"reachable": True,
|
||||||
|
"alert": "none",
|
||||||
|
"tholddark": 12467,
|
||||||
|
"tholdoffset": 7000,
|
||||||
|
"ledindication": False,
|
||||||
|
"usertest": False,
|
||||||
|
"pending": []
|
||||||
|
},
|
||||||
|
"name": "Hue ambient light sensor 3",
|
||||||
|
"type": "ZLLLightLevel",
|
||||||
|
"modelid": "SML001",
|
||||||
|
"manufacturername": "Philips",
|
||||||
|
"productname": "Hue ambient light sensor",
|
||||||
|
"swversion": "6.1.1.27575",
|
||||||
|
"uniqueid": "00:11:22:33:44:55:66:99-02-0400",
|
||||||
|
"capabilities": {
|
||||||
|
"certified": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TEMPERATURE_SENSOR_3 = {
|
||||||
|
"state": {
|
||||||
|
"temperature": 1775,
|
||||||
|
"lastupdated": "2019-01-01T01:00:00"
|
||||||
|
},
|
||||||
|
"swupdate": {
|
||||||
|
"state": "noupdates",
|
||||||
|
"lastinstall": "2019-01-01T01:00:00"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"on": True,
|
||||||
|
"battery": 100,
|
||||||
|
"reachable": True,
|
||||||
|
"alert": "none",
|
||||||
|
"ledindication": False,
|
||||||
|
"usertest": False,
|
||||||
|
"pending": []
|
||||||
|
},
|
||||||
|
"name": "Hue temperature sensor 3",
|
||||||
|
"type": "ZLLTemperature",
|
||||||
|
"modelid": "SML001",
|
||||||
|
"manufacturername": "Philips",
|
||||||
|
"productname": "Hue temperature sensor",
|
||||||
|
"swversion": "6.1.1.27575",
|
||||||
|
"uniqueid": "00:11:22:33:44:55:66:99-02-0402",
|
||||||
|
"capabilities": {
|
||||||
|
"certified": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UNSUPPORTED_SENSOR = {
|
||||||
|
"state": {
|
||||||
|
"status": 0,
|
||||||
|
"lastupdated": "2019-01-01T01:00:00"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"on": True,
|
||||||
|
"reachable": True
|
||||||
|
},
|
||||||
|
"name": "Unsupported sensor",
|
||||||
|
"type": "CLIPGenericStatus",
|
||||||
|
"modelid": "PHWA01",
|
||||||
|
"manufacturername": "Philips",
|
||||||
|
"swversion": "1.0",
|
||||||
|
"uniqueid": "arbitrary",
|
||||||
|
"recycle": True
|
||||||
|
}
|
||||||
|
SENSOR_RESPONSE = {
|
||||||
|
"1": PRESENCE_SENSOR_1_PRESENT,
|
||||||
|
"2": LIGHT_LEVEL_SENSOR_1,
|
||||||
|
"3": TEMPERATURE_SENSOR_1,
|
||||||
|
"4": PRESENCE_SENSOR_2_NOT_PRESENT,
|
||||||
|
"5": LIGHT_LEVEL_SENSOR_2,
|
||||||
|
"6": TEMPERATURE_SENSOR_2,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_bridge(hass):
|
||||||
|
"""Mock a Hue bridge."""
|
||||||
|
bridge = Mock(
|
||||||
|
available=True,
|
||||||
|
allow_unreachable=False,
|
||||||
|
allow_groups=False,
|
||||||
|
api=Mock(),
|
||||||
|
spec=hue.HueBridge
|
||||||
|
)
|
||||||
|
bridge.mock_requests = []
|
||||||
|
# We're using a deque so we can schedule multiple responses
|
||||||
|
# and also means that `popleft()` will blow up if we get more updates
|
||||||
|
# than expected.
|
||||||
|
bridge.mock_sensor_responses = deque()
|
||||||
|
|
||||||
|
async def mock_request(method, path, **kwargs):
|
||||||
|
kwargs['method'] = method
|
||||||
|
kwargs['path'] = path
|
||||||
|
bridge.mock_requests.append(kwargs)
|
||||||
|
|
||||||
|
if path == 'sensors':
|
||||||
|
return bridge.mock_sensor_responses.popleft()
|
||||||
|
return None
|
||||||
|
|
||||||
|
bridge.api.config.apiversion = '9.9.9'
|
||||||
|
bridge.api.sensors = Sensors({}, mock_request)
|
||||||
|
|
||||||
|
return bridge
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def increase_scan_interval(hass):
|
||||||
|
"""Increase the SCAN_INTERVAL to prevent unexpected scans during tests."""
|
||||||
|
hue_sensor_base.SensorManager.SCAN_INTERVAL = datetime.timedelta(days=365)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_bridge(hass, mock_bridge):
|
||||||
|
"""Load the Hue platform with the provided bridge."""
|
||||||
|
hass.config.components.add(hue.DOMAIN)
|
||||||
|
hass.data[hue.DOMAIN] = {'mock-host': mock_bridge}
|
||||||
|
config_entry = config_entries.ConfigEntry(1, hue.DOMAIN, 'Mock Title', {
|
||||||
|
'host': 'mock-host'
|
||||||
|
}, 'test', config_entries.CONN_CLASS_LOCAL_POLL)
|
||||||
|
await hass.config_entries.async_forward_entry_setup(
|
||||||
|
config_entry, 'binary_sensor')
|
||||||
|
await hass.config_entries.async_forward_entry_setup(
|
||||||
|
config_entry, 'sensor')
|
||||||
|
# and make sure it completes before going further
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_sensors(hass, mock_bridge):
|
||||||
|
"""Test the update_items function when no sensors are found."""
|
||||||
|
mock_bridge.allow_groups = True
|
||||||
|
mock_bridge.mock_sensor_responses.append({})
|
||||||
|
await setup_bridge(hass, mock_bridge)
|
||||||
|
assert len(mock_bridge.mock_requests) == 1
|
||||||
|
assert len(hass.states.async_all()) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sensors(hass, mock_bridge):
|
||||||
|
"""Test the update_items function with some sensors."""
|
||||||
|
mock_bridge.mock_sensor_responses.append(SENSOR_RESPONSE)
|
||||||
|
await setup_bridge(hass, mock_bridge)
|
||||||
|
assert len(mock_bridge.mock_requests) == 1
|
||||||
|
# 2 "physical" sensors with 3 virtual sensors each
|
||||||
|
assert len(hass.states.async_all()) == 6
|
||||||
|
|
||||||
|
presence_sensor_1 = hass.states.get(
|
||||||
|
'binary_sensor.living_room_sensor_presence')
|
||||||
|
light_level_sensor_1 = hass.states.get(
|
||||||
|
'sensor.living_room_sensor_light_level')
|
||||||
|
temperature_sensor_1 = hass.states.get(
|
||||||
|
'sensor.living_room_sensor_temperature')
|
||||||
|
assert presence_sensor_1 is not None
|
||||||
|
assert presence_sensor_1.state == 'on'
|
||||||
|
assert light_level_sensor_1 is not None
|
||||||
|
assert light_level_sensor_1.state == '0'
|
||||||
|
assert light_level_sensor_1.name == 'Living room sensor light level'
|
||||||
|
assert temperature_sensor_1 is not None
|
||||||
|
assert temperature_sensor_1.state == '17.75'
|
||||||
|
assert temperature_sensor_1.name == 'Living room sensor temperature'
|
||||||
|
|
||||||
|
presence_sensor_2 = hass.states.get(
|
||||||
|
'binary_sensor.kitchen_sensor_presence')
|
||||||
|
light_level_sensor_2 = hass.states.get(
|
||||||
|
'sensor.kitchen_sensor_light_level')
|
||||||
|
temperature_sensor_2 = hass.states.get(
|
||||||
|
'sensor.kitchen_sensor_temperature')
|
||||||
|
assert presence_sensor_2 is not None
|
||||||
|
assert presence_sensor_2.state == 'off'
|
||||||
|
assert light_level_sensor_2 is not None
|
||||||
|
assert light_level_sensor_2.state == '100'
|
||||||
|
assert light_level_sensor_2.name == 'Kitchen sensor light level'
|
||||||
|
assert temperature_sensor_2 is not None
|
||||||
|
assert temperature_sensor_2.state == '18.75'
|
||||||
|
assert temperature_sensor_2.name == 'Kitchen sensor temperature'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unsupported_sensors(hass, mock_bridge):
|
||||||
|
"""Test that unsupported sensors don't get added and don't fail."""
|
||||||
|
response_with_unsupported = dict(SENSOR_RESPONSE)
|
||||||
|
response_with_unsupported['7'] = UNSUPPORTED_SENSOR
|
||||||
|
mock_bridge.mock_sensor_responses.append(response_with_unsupported)
|
||||||
|
await setup_bridge(hass, mock_bridge)
|
||||||
|
assert len(mock_bridge.mock_requests) == 1
|
||||||
|
# 2 "physical" sensors with 3 virtual sensors each
|
||||||
|
assert len(hass.states.async_all()) == 6
|
||||||
|
|
||||||
|
|
||||||
|
async def test_new_sensor_discovered(hass, mock_bridge):
|
||||||
|
"""Test if 2nd update has a new sensor."""
|
||||||
|
mock_bridge.mock_sensor_responses.append(SENSOR_RESPONSE)
|
||||||
|
|
||||||
|
await setup_bridge(hass, mock_bridge)
|
||||||
|
assert len(mock_bridge.mock_requests) == 1
|
||||||
|
assert len(hass.states.async_all()) == 6
|
||||||
|
|
||||||
|
new_sensor_response = dict(SENSOR_RESPONSE)
|
||||||
|
new_sensor_response.update({
|
||||||
|
"7": PRESENCE_SENSOR_3_PRESENT,
|
||||||
|
"8": LIGHT_LEVEL_SENSOR_3,
|
||||||
|
"9": TEMPERATURE_SENSOR_3,
|
||||||
|
})
|
||||||
|
|
||||||
|
mock_bridge.mock_sensor_responses.append(new_sensor_response)
|
||||||
|
|
||||||
|
# Force updates to run again
|
||||||
|
sm = hass.data[hue.DOMAIN][hue_sensor_base.SENSOR_MANAGER]
|
||||||
|
await sm.async_update_items()
|
||||||
|
|
||||||
|
# To flush out the service call to update the group
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(mock_bridge.mock_requests) == 2
|
||||||
|
assert len(hass.states.async_all()) == 9
|
||||||
|
|
||||||
|
presence = hass.states.get('binary_sensor.bedroom_sensor_presence')
|
||||||
|
assert presence is not None
|
||||||
|
assert presence.state == 'on'
|
||||||
|
temperature = hass.states.get('sensor.bedroom_sensor_temperature')
|
||||||
|
assert temperature is not None
|
||||||
|
assert temperature.state == '17.75'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_timeout(hass, mock_bridge):
|
||||||
|
"""Test bridge marked as not available if timeout error during update."""
|
||||||
|
mock_bridge.api.sensors.update = Mock(side_effect=asyncio.TimeoutError)
|
||||||
|
await setup_bridge(hass, mock_bridge)
|
||||||
|
assert len(mock_bridge.mock_requests) == 0
|
||||||
|
assert len(hass.states.async_all()) == 0
|
||||||
|
assert mock_bridge.available is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_unauthorized(hass, mock_bridge):
|
||||||
|
"""Test bridge marked as not available if unauthorized during update."""
|
||||||
|
mock_bridge.api.sensors.update = Mock(side_effect=aiohue.Unauthorized)
|
||||||
|
await setup_bridge(hass, mock_bridge)
|
||||||
|
assert len(mock_bridge.mock_requests) == 0
|
||||||
|
assert len(hass.states.async_all()) == 0
|
||||||
|
assert mock_bridge.available is False
|
Loading…
x
Reference in New Issue
Block a user