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:
Richard Mitchell 2019-04-18 06:13:03 +01:00 committed by Paulus Schoutsen
parent 77244eab1e
commit 474ac8b09e
6 changed files with 875 additions and 7 deletions

View 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

View File

@ -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."""

View 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

View 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
}

View File

@ -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

View 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