mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +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(
|
||||
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(
|
||||
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
|
||||
# register service
|
||||
return await self.hass.config_entries.async_forward_entry_unload(
|
||||
self.config_entry, 'light')
|
||||
results = await asyncio.gather(
|
||||
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):
|
||||
"""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 hue_bridge.api is api
|
||||
assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 1
|
||||
assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == \
|
||||
(entry, 'light')
|
||||
forward_entries = set(
|
||||
c[1][1]
|
||||
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():
|
||||
@ -84,11 +88,11 @@ async def test_reset_unloads_entry_if_setup():
|
||||
assert await hue_bridge.async_setup() is True
|
||||
|
||||
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 = \
|
||||
mock_coro(True)
|
||||
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
|
||||
|
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