From 9d93c43511544feb7ef97388add84f91465aed43 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Sun, 31 Mar 2019 20:37:52 +0100 Subject: [PATCH 01/28] Add basic support for native Hue sensors --- homeassistant/components/hue/__init__.py | 9 +- homeassistant/components/hue/bridge.py | 5 +- homeassistant/components/hue/sensor.py | 344 +++++++++++++++++++++++ 3 files changed, 356 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/hue/sensor.py diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index ac17e6e852f435..48d9ae3fc3515e 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -28,6 +28,9 @@ CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" DEFAULT_ALLOW_HUE_GROUPS = True +CONF_ALLOW_HUE_SENSORS = "allow_hue_sensors" +DEFAULT_ALLOW_HUE_SENSORS = True + BRIDGE_CONFIG_SCHEMA = vol.Schema({ # Validate as IP address and then convert back to a string. vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string), @@ -37,6 +40,8 @@ default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean, vol.Optional(CONF_ALLOW_HUE_GROUPS, default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean, + vol.Optional(CONF_ALLOW_HUE_SENSORS, + default=DEFAULT_ALLOW_HUE_SENSORS): cv.boolean, }) CONFIG_SCHEMA = vol.Schema({ @@ -97,11 +102,13 @@ async def async_setup_entry(hass, entry): if config is None: allow_unreachable = DEFAULT_ALLOW_UNREACHABLE allow_groups = DEFAULT_ALLOW_HUE_GROUPS + allow_sensors = DEFAULT_ALLOW_HUE_SENSORS else: allow_unreachable = config[CONF_ALLOW_UNREACHABLE] allow_groups = config[CONF_ALLOW_HUE_GROUPS] + allow_sensors = config[CONF_ALLOW_HUE_SENSORS] - bridge = HueBridge(hass, entry, allow_unreachable, allow_groups) + bridge = HueBridge(hass, entry, allow_unreachable, allow_groups, allow_sensors) if not await bridge.async_setup(): return False diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 9e99d219316aee..a76a4867a025fd 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -24,12 +24,13 @@ class HueBridge: """Manages a single Hue bridge.""" - def __init__(self, hass, config_entry, allow_unreachable, allow_groups): + def __init__(self, hass, config_entry, allow_unreachable, allow_groups, allow_sensors): """Initialize the system.""" self.config_entry = config_entry self.hass = hass self.allow_unreachable = allow_unreachable self.allow_groups = allow_groups + self.allow_sensors = allow_sensors self.available = True self.api = None @@ -69,6 +70,8 @@ async def async_setup(self, tries=0): 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, 'sensor')) hass.services.async_register( DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py new file mode 100644 index 00000000000000..34e05e1d50eed4 --- /dev/null +++ b/homeassistant/components/hue/sensor.py @@ -0,0 +1,344 @@ +"""Support for the Philips Hue sensors.""" +import asyncio +from datetime import timedelta +import logging +from time import monotonic +import random + +import async_timeout + +from homeassistant.components import hue +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, ENTITY_ID_FORMAT as BINARY_ENTITY_ID_FORMAT) +from homeassistant.components.sensor import ENTITY_ID_FORMAT as SENSOR_ENTITY_ID_FORMAT +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) +from homeassistant.helpers.entity import Entity, async_generate_entity_id + +DEPENDENCIES = ['hue'] +SCAN_INTERVAL = timedelta(seconds=5) + +PRESENCE_NAME_FORMAT = "{} presence" +LIGHT_LEVEL_NAME_FORMAT = "{} light level" +TEMPERATURE_NAME_FORMAT = "{} temperature" + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Hue sensors from a config entry.""" + bridge = hass.data[hue.DOMAIN][config_entry.data['host']] + cur_sensors = {} + + allow_sensors = bridge.allow_sensors + if not allow_sensors: + _LOGGER.info('Skipping Hue sensor setup. Set allow_hue_sensors to true if you don\'t want this.') + return + + # Hue updates all sensors via a single API call. + # + # If we call a service to update 2 sensors, we only want the API to be + # called once. + # + # The throttle decorator will return right away if a call is currently + # in progress. This means that if we are updating 2 sensors, the first one + # is in the update method, the second one will skip it and assume the + # update went through and updates it's data, not good! + # + # The current mechanism will make sure that all sensors will wait till + # the update call is done before writing their data to the state machine. + # + # An alternative approach would be to disable automatic polling by Home + # Assistant and take control ourselves. This works great for polling as now + # we trigger from 1 time update an update to all entities. However it gets + # tricky from inside async_turn_on and async_turn_off. + # + # If automatic polling is enabled, Home Assistant will call the entity + # update method after it is done calling all the services. This means that + # when we update, we know all commands have been processed. If we trigger + # the update from inside async_turn_on, the update will not capture the + # changes to the second entity until the next polling update because the + # throttle decorator will prevent the call. + + progress = None + sensor_progress = set() + + async def request_update(object_id): + """Request an update. + + We will only make 1 request to the server for updating at a time. If a + request is in progress, we will join the request that is in progress. + + This approach is possible because should_poll=True. That means that + Home Assistant will ask sensors for updates during a polling cycle or + after it has called a service. + + We keep track of the sensors that are waiting for the request to finish. + When new data comes in, we'll trigger an update for all non-waiting + sensors. This covers the case where a service is called to enable 2 + sensors but in the meanwhile some other sensor has changed too. + """ + nonlocal progress + + sensor_progress.add(object_id) + + if progress is not None: + return await progress + + progress = asyncio.ensure_future(update_bridge()) + result = await progress + progress = None + sensor_progress.clear() + return result + + async def update_bridge(): + """Update the values of the bridge. + + Will update sensors from the bridge. + """ + tasks = [] + tasks.append(async_update_items( + hass, bridge, async_add_entities, request_update, cur_sensors, + sensor_progress + )) + + await asyncio.wait(tasks) + + await update_bridge() + + +async def async_update_items(hass, bridge, async_add_entities, + request_bridge_update, current, progress_waiting): + """Update sensors from the bridge.""" + import aiohue + + api = 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 bridge.available: + return + + _LOGGER.error('Unable to reach bridge %s (%s)', bridge.host, err) + bridge.available = False + + for sensor_id, sensor in current.items(): + if sensor_id not in progress_waiting: + sensor.async_schedule_update_ha_state() + + return + + finally: + _LOGGER.debug('Finished sensor request in %.3f seconds', + monotonic() - start) + + if not bridge.available: + _LOGGER.info('Reconnected to bridge %s', bridge.host) + bridge.available = True + + new_sensors = [] + sensor_device_names = {} + + for item_id in api: + if item_id not in current: + name = PRESENCE_NAME_FORMAT.format(api[item_id].name) + if api[item_id].type == aiohue.sensors.TYPE_ZLL_PRESENCE: + s = HuePresence( + hass, api[item_id], name, request_bridge_update, + bridge) + sensor_device_names[s.device_id] = api[item_id].name + current[item_id] = s + + # Iterate again now we have all the presence sensors, and add the related + # sensors with nice names + for item_id in api: + if item_id not in current: + device_id = api[item_id].uniqueid + if device_id and len(device_id) > 23: + device_id = device_id[:23] + name = api[item_id].name + if api[item_id].type == aiohue.sensors.TYPE_ZLL_LIGHTLEVEL: + _LOGGER.info('Found light level sensor %s (%s)', device_id, api[item_id].uniqueid) + if device_id in sensor_device_names: + name = LIGHT_LEVEL_NAME_FORMAT.format( + sensor_device_names[device_id]) + _LOGGER.info('Light level sensor name: %s', name) + current[item_id] = HueLightLevel( + hass, api[item_id], name, request_bridge_update, bridge) + elif api[item_id].type == aiohue.sensors.TYPE_ZLL_TEMPERATURE: + _LOGGER.info('Found temperature sensor %s (%s)', device_id, api[item_id].uniqueid) + if device_id in sensor_device_names: + name = TEMPERATURE_NAME_FORMAT.format( + sensor_device_names[device_id]) + _LOGGER.info('Temperature sensor name: %s', name) + current[item_id] = HueTemperature( + hass, api[item_id], name, request_bridge_update, bridge) + + if item_id in current: + new_sensors.append(current[item_id]) + + elif item_id not in progress_waiting: + current[item_id].async_schedule_update_ha_state() + + if new_sensors: + async_add_entities(new_sensors) + + +class GenericHueSensor: + """Representation of a Hue sensor.""" + + def __init__(self, hass, sensor, name, request_bridge_update, bridge): + """Initialize the sensor.""" + self.hass = hass + self.sensor = sensor + self._name = name + self.async_request_bridge_update = request_bridge_update + self.bridge = bridge + + if self.swupdatestate == "readytoinstall": + err = ( + "Please check for software updates of the %s " + "sensor in the Philips Hue App." + ) + _LOGGER.warning(err, self.name) + + self.entity_id = async_generate_entity_id( + self._entity_id_format, self.name, hass=hass) + + @property + def device_id(self): + 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 self.sensor.raw.get('swupdate', {}).get('state') + + @property + def device_info(self): + """Return the device info.""" + return { + 'identifiers': { + (hue.DOMAIN, self.unique_id) + }, + 'name': self.name, + 'manufacturer': self.sensor.manufacturername, + # productname added in Hue Bridge API 1.24 + # (published 03/05/2018) + 'model': self.sensor.productname or self.sensor.modelid, + # Not yet exposed as properties in aiohue + 'sw_version': self.sensor.swversion, + 'via_hub': (hue.DOMAIN, self.bridge.api.config.bridgeid), + } + + async def async_update(self): + """Synchronize state with bridge.""" + await self.async_request_bridge_update(self.sensor.id) + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = {} + return attributes + + +class GenericZLLSensor(GenericHueSensor): + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = super().device_state_attributes + attributes.update({ + "battery": self.sensor.battery, + "last_updated": self.sensor.lastupdated, + "on": self.sensor.on, + "reachable": self.sensor.reachable, + }) + return attributes + + +class HueLightLevel(GenericZLLSensor, Entity): + + _entity_id_format = SENSOR_ENTITY_ID_FORMAT + + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_ILLUMINANCE + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return "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({ + "is_dark": self.sensor.dark, + "is_daylight": self.sensor.daylight, + "threshold_dark": self.sensor.tholddark, + "threshold_offset": self.sensor.tholdoffset, + }) + return attributes + + +class HueTemperature(GenericZLLSensor, Entity): + + _entity_id_format = SENSOR_ENTITY_ID_FORMAT + + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_TEMPERATURE + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return TEMP_CELSIUS + + @property + def state(self): + """Return the state of the device.""" + return self.sensor.temperature / 100 + + +class HuePresence(GenericZLLSensor, BinarySensorDevice): + + _entity_id_format = BINARY_ENTITY_ID_FORMAT + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self.sensor.presence + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return 'mdi:run' From 0be274e9493552b0fd8fc813bcaaccc10a855217 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Sun, 31 Mar 2019 20:42:58 +0100 Subject: [PATCH 02/28] Update coveragerc --- .coveragerc | 1 + homeassistant/components/hue/__init__.py | 3 ++- homeassistant/components/hue/bridge.py | 3 ++- homeassistant/components/hue/sensor.py | 23 +++++++++++------------ 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.coveragerc b/.coveragerc index 86819ef51a39b8..411e9aac5990f0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -262,6 +262,7 @@ omit = homeassistant/components/huawei_lte/* homeassistant/components/huawei_router/device_tracker.py homeassistant/components/hue/light.py + homeassistant/components/hue/sensor.py homeassistant/components/hunterdouglas_powerview/scene.py homeassistant/components/hydrawise/* homeassistant/components/hyperion/light.py diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 48d9ae3fc3515e..19a42b4bba46e9 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -108,7 +108,8 @@ async def async_setup_entry(hass, entry): allow_groups = config[CONF_ALLOW_HUE_GROUPS] allow_sensors = config[CONF_ALLOW_HUE_SENSORS] - bridge = HueBridge(hass, entry, allow_unreachable, allow_groups, allow_sensors) + bridge = HueBridge( + hass, entry, allow_unreachable, allow_groups, allow_sensors) if not await bridge.async_setup(): return False diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index a76a4867a025fd..f02ba55869fc5f 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -24,7 +24,8 @@ class HueBridge: """Manages a single Hue bridge.""" - def __init__(self, hass, config_entry, allow_unreachable, allow_groups, allow_sensors): + def __init__(self, hass, config_entry, allow_unreachable, allow_groups, + allow_sensors): """Initialize the system.""" self.config_entry = config_entry self.hass = hass diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 34e05e1d50eed4..964fdabb6c7986 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -3,16 +3,16 @@ from datetime import timedelta import logging from time import monotonic -import random import async_timeout from homeassistant.components import hue from homeassistant.components.binary_sensor import ( BinarySensorDevice, ENTITY_ID_FORMAT as BINARY_ENTITY_ID_FORMAT) -from homeassistant.components.sensor import ENTITY_ID_FORMAT as SENSOR_ENTITY_ID_FORMAT +from homeassistant.components.sensor import ( + ENTITY_ID_FORMAT as SENSOR_ENTITY_ID_FORMAT) from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) + DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) from homeassistant.helpers.entity import Entity, async_generate_entity_id DEPENDENCIES = ['hue'] @@ -32,7 +32,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): allow_sensors = bridge.allow_sensors if not allow_sensors: - _LOGGER.info('Skipping Hue sensor setup. Set allow_hue_sensors to true if you don\'t want this.') + _LOGGER.info( + 'Skipping Hue sensor setup. Set allow_hue_sensors to true if you ' + 'don\'t want this.') return # Hue updates all sensors via a single API call. @@ -73,10 +75,11 @@ async def request_update(object_id): Home Assistant will ask sensors for updates during a polling cycle or after it has called a service. - We keep track of the sensors that are waiting for the request to finish. - When new data comes in, we'll trigger an update for all non-waiting - sensors. This covers the case where a service is called to enable 2 - sensors but in the meanwhile some other sensor has changed too. + We keep track of the sensors that are waiting for the request to + finish. When new data comes in, we'll trigger an update for all + non-waiting sensors. This covers the case where a service is called to + enable 2 sensors but in the meanwhile some other sensor has changed + too. """ nonlocal progress @@ -163,19 +166,15 @@ async def async_update_items(hass, bridge, async_add_entities, device_id = device_id[:23] name = api[item_id].name if api[item_id].type == aiohue.sensors.TYPE_ZLL_LIGHTLEVEL: - _LOGGER.info('Found light level sensor %s (%s)', device_id, api[item_id].uniqueid) if device_id in sensor_device_names: name = LIGHT_LEVEL_NAME_FORMAT.format( sensor_device_names[device_id]) - _LOGGER.info('Light level sensor name: %s', name) current[item_id] = HueLightLevel( hass, api[item_id], name, request_bridge_update, bridge) elif api[item_id].type == aiohue.sensors.TYPE_ZLL_TEMPERATURE: - _LOGGER.info('Found temperature sensor %s (%s)', device_id, api[item_id].uniqueid) if device_id in sensor_device_names: name = TEMPERATURE_NAME_FORMAT.format( sensor_device_names[device_id]) - _LOGGER.info('Temperature sensor name: %s', name) current[item_id] = HueTemperature( hass, api[item_id], name, request_bridge_update, bridge) From bc0a53068a30cdc9cf353691fdf9e29454a30861 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Sun, 31 Mar 2019 20:55:48 +0100 Subject: [PATCH 03/28] Simplify attributes --- homeassistant/components/hue/sensor.py | 31 +++++--------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 964fdabb6c7986..3f0fe319f880ca 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -279,16 +279,8 @@ def device_state_attributes(self): class HueLightLevel(GenericZLLSensor, Entity): _entity_id_format = SENSOR_ENTITY_ID_FORMAT - - @property - def device_class(self): - """Return the device class of the sensor.""" - return DEVICE_CLASS_ILLUMINANCE - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return "Lux" + device_class = DEVICE_CLASS_ILLUMINANCE + unit_of_measurement = "Lux" @property def state(self): @@ -311,16 +303,8 @@ def device_state_attributes(self): class HueTemperature(GenericZLLSensor, Entity): _entity_id_format = SENSOR_ENTITY_ID_FORMAT - - @property - def device_class(self): - """Return the device class of the sensor.""" - return DEVICE_CLASS_TEMPERATURE - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return TEMP_CELSIUS + device_class = DEVICE_CLASS_TEMPERATURE + unit_of_measurement = TEMP_CELSIUS @property def state(self): @@ -331,13 +315,10 @@ def state(self): class HuePresence(GenericZLLSensor, BinarySensorDevice): _entity_id_format = BINARY_ENTITY_ID_FORMAT + device_class = 'presence' + icon = 'mdi:run' @property def is_on(self): """Return true if the binary sensor is on.""" return self.sensor.presence - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return 'mdi:run' From 83cda85278112a424e349aac63a3588d711a3f1b Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Mon, 1 Apr 2019 10:59:41 +0100 Subject: [PATCH 04/28] Remove config option --- homeassistant/components/hue/__init__.py | 5 ----- homeassistant/components/hue/sensor.py | 7 ------- 2 files changed, 12 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 19a42b4bba46e9..363a03a10cbff1 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -28,9 +28,6 @@ CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" DEFAULT_ALLOW_HUE_GROUPS = True -CONF_ALLOW_HUE_SENSORS = "allow_hue_sensors" -DEFAULT_ALLOW_HUE_SENSORS = True - BRIDGE_CONFIG_SCHEMA = vol.Schema({ # Validate as IP address and then convert back to a string. vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string), @@ -40,8 +37,6 @@ default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean, vol.Optional(CONF_ALLOW_HUE_GROUPS, default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean, - vol.Optional(CONF_ALLOW_HUE_SENSORS, - default=DEFAULT_ALLOW_HUE_SENSORS): cv.boolean, }) CONFIG_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 3f0fe319f880ca..159c9681d54dba 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -30,13 +30,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): bridge = hass.data[hue.DOMAIN][config_entry.data['host']] cur_sensors = {} - allow_sensors = bridge.allow_sensors - if not allow_sensors: - _LOGGER.info( - 'Skipping Hue sensor setup. Set allow_hue_sensors to true if you ' - 'don\'t want this.') - return - # Hue updates all sensors via a single API call. # # If we call a service to update 2 sensors, we only want the API to be From 698a3a3cd89fbd42d3fcdbbcf476f463990479df Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Mon, 1 Apr 2019 11:00:08 +0100 Subject: [PATCH 05/28] Refactor and document device-ness and update mechanism --- homeassistant/components/hue/sensor.py | 245 +++++++++++-------------- 1 file changed, 104 insertions(+), 141 deletions(-) diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 159c9681d54dba..d4ee448c7e9531 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -14,6 +14,9 @@ from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util.dt import utcnow + DEPENDENCIES = ['hue'] SCAN_INTERVAL = timedelta(seconds=5) @@ -30,81 +33,22 @@ async def async_setup_entry(hass, config_entry, async_add_entities): bridge = hass.data[hue.DOMAIN][config_entry.data['host']] cur_sensors = {} - # Hue updates all sensors via a single API call. - # - # If we call a service to update 2 sensors, we only want the API to be - # called once. - # - # The throttle decorator will return right away if a call is currently - # in progress. This means that if we are updating 2 sensors, the first one - # is in the update method, the second one will skip it and assume the - # update went through and updates it's data, not good! - # - # The current mechanism will make sure that all sensors will wait till - # the update call is done before writing their data to the state machine. - # - # An alternative approach would be to disable automatic polling by Home - # Assistant and take control ourselves. This works great for polling as now - # we trigger from 1 time update an update to all entities. However it gets - # tricky from inside async_turn_on and async_turn_off. - # - # If automatic polling is enabled, Home Assistant will call the entity - # update method after it is done calling all the services. This means that - # when we update, we know all commands have been processed. If we trigger - # the update from inside async_turn_on, the update will not capture the - # changes to the second entity until the next polling update because the - # throttle decorator will prevent the call. - - progress = None - sensor_progress = set() - - async def request_update(object_id): - """Request an update. - - We will only make 1 request to the server for updating at a time. If a - request is in progress, we will join the request that is in progress. - - This approach is possible because should_poll=True. That means that - Home Assistant will ask sensors for updates during a polling cycle or - after it has called a service. - - We keep track of the sensors that are waiting for the request to - finish. When new data comes in, we'll trigger an update for all - non-waiting sensors. This covers the case where a service is called to - enable 2 sensors but in the meanwhile some other sensor has changed - too. - """ - nonlocal progress - - sensor_progress.add(object_id) - - if progress is not None: - return await progress - - progress = asyncio.ensure_future(update_bridge()) - result = await progress - progress = None - sensor_progress.clear() - return result - - async def update_bridge(): + async def async_update_bridge(now): """Update the values of the bridge. Will update sensors from the bridge. """ - tasks = [] - tasks.append(async_update_items( - hass, bridge, async_add_entities, request_update, cur_sensors, - sensor_progress - )) - await asyncio.wait(tasks) + await async_update_items( + hass, bridge, async_add_entities, cur_sensors) + + async_track_point_in_utc_time( + hass, async_update_bridge, utcnow() + SCAN_INTERVAL) - await update_bridge() + await async_update_bridge(None) -async def async_update_items(hass, bridge, async_add_entities, - request_bridge_update, current, progress_waiting): +async def async_update_items(hass, bridge, async_add_entities, current): """Update sensors from the bridge.""" import aiohue @@ -123,10 +67,6 @@ async def async_update_items(hass, bridge, async_add_entities, _LOGGER.error('Unable to reach bridge %s (%s)', bridge.host, err) bridge.available = False - for sensor_id, sensor in current.items(): - if sensor_id not in progress_waiting: - sensor.async_schedule_update_ha_state() - return finally: @@ -140,42 +80,61 @@ async def async_update_items(hass, bridge, async_add_entities, new_sensors = [] sensor_device_names = {} + # 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 item_id not in current: - name = PRESENCE_NAME_FORMAT.format(api[item_id].name) - if api[item_id].type == aiohue.sensors.TYPE_ZLL_PRESENCE: - s = HuePresence( - hass, api[item_id], name, request_bridge_update, - bridge) - sensor_device_names[s.device_id] = api[item_id].name - current[item_id] = s + if item_id in current: + continue + + name = PRESENCE_NAME_FORMAT.format(api[item_id].name) + if api[item_id].type == aiohue.sensors.TYPE_ZLL_PRESENCE: + s = HuePresence(hass, api[item_id], name, bridge) + sensor_device_names[s.device_id] = api[item_id] + current[item_id] = s + new_sensors.append(s) # Iterate again now we have all the presence sensors, and add the related - # sensors with nice names + # sensors with nice names. for item_id in api: - if item_id not in current: - device_id = api[item_id].uniqueid - if device_id and len(device_id) > 23: - device_id = device_id[:23] - name = api[item_id].name - if api[item_id].type == aiohue.sensors.TYPE_ZLL_LIGHTLEVEL: - if device_id in sensor_device_names: - name = LIGHT_LEVEL_NAME_FORMAT.format( - sensor_device_names[device_id]) - current[item_id] = HueLightLevel( - hass, api[item_id], name, request_bridge_update, bridge) - elif api[item_id].type == aiohue.sensors.TYPE_ZLL_TEMPERATURE: - if device_id in sensor_device_names: - name = TEMPERATURE_NAME_FORMAT.format( - sensor_device_names[device_id]) - current[item_id] = HueTemperature( - hass, api[item_id], name, request_bridge_update, bridge) - - if item_id in current: - new_sensors.append(current[item_id]) - - elif item_id not in progress_waiting: - current[item_id].async_schedule_update_ha_state() + if item_id in current: + continue + + # Work out the shared device ID, as described above + device_id = api[item_id].uniqueid + if device_id and len(device_id) > 23: + device_id = device_id[:23] + name = api[item_id].name + primary_sensor = None + if api[item_id].type == aiohue.sensors.TYPE_ZLL_LIGHTLEVEL: + if device_id in sensor_device_names: + primary_sensor = sensor_device_names[device_id] + name = LIGHT_LEVEL_NAME_FORMAT.format( + primary_sensor.name) + current[item_id] = HueLightLevel( + hass, api[item_id], name, bridge, + primary_sensor=primary_sensor) + elif api[item_id].type == aiohue.sensors.TYPE_ZLL_TEMPERATURE: + if device_id in sensor_device_names: + primary_sensor = sensor_device_names[device_id] + name = TEMPERATURE_NAME_FORMAT.format( + primary_sensor.name) + current[item_id] = HueTemperature( + hass, api[item_id], name, bridge, + primary_sensor=primary_sensor) + else: + continue + + new_sensors.append(current[item_id]) if new_sensors: async_add_entities(new_sensors) @@ -184,12 +143,15 @@ async def async_update_items(hass, bridge, async_add_entities, class GenericHueSensor: """Representation of a Hue sensor.""" - def __init__(self, hass, sensor, name, request_bridge_update, bridge): + should_poll = False + + def __init__(self, hass, sensor, name, bridge, + primary_sensor=None): """Initialize the sensor.""" self.hass = hass self.sensor = sensor + self._primary_sensor = primary_sensor self._name = name - self.async_request_bridge_update = request_bridge_update self.bridge = bridge if self.swupdatestate == "readytoinstall": @@ -202,8 +164,19 @@ def __init__(self, hass, sensor, name, request_bridge_update, bridge): self.entity_id = async_generate_entity_id( self._entity_id_format, self.name, hass=hass) + @property + def primary_sensor(self): + """Return the entity which represents the primary sensor of + this device. + """ + + return self._primary_sensor or self.sensor + @property def device_id(self): + """Return the ID that represents the physical device this + sensor is part of. + """ return self.unique_id[:23] @property @@ -224,49 +197,51 @@ def available(self): @property def swupdatestate(self): - return self.sensor.raw.get('swupdate', {}).get('state') + """The state of available software updates for this device.""" + return self.primary_sensor.raw.get('swupdate', {}).get('state') @property def device_info(self): - """Return the device info.""" + """Return the device info to link individual entities together + in the hass device registry. + """ + return { 'identifiers': { - (hue.DOMAIN, self.unique_id) + (hue.DOMAIN, self.device_id) }, - 'name': self.name, - 'manufacturer': self.sensor.manufacturername, - # productname added in Hue Bridge API 1.24 - # (published 03/05/2018) - 'model': self.sensor.productname or self.sensor.modelid, - # Not yet exposed as properties in aiohue - 'sw_version': self.sensor.swversion, + '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), } - async def async_update(self): - """Synchronize state with bridge.""" - await self.async_request_bridge_update(self.sensor.id) - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - attributes = {} - return attributes - class GenericZLLSensor(GenericHueSensor): @property def device_state_attributes(self): """Return the device state attributes.""" - attributes = super().device_state_attributes - attributes.update({ + return { "battery": self.sensor.battery, "last_updated": self.sensor.lastupdated, "on": self.sensor.on, "reachable": self.sensor.reachable, - }) - return attributes + } + + +class HuePresence(GenericZLLSensor, BinarySensorDevice): + + _entity_id_format = BINARY_ENTITY_ID_FORMAT + device_class = 'presence' + icon = 'mdi:run' + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self.sensor.presence class HueLightLevel(GenericZLLSensor, Entity): @@ -303,15 +278,3 @@ class HueTemperature(GenericZLLSensor, Entity): def state(self): """Return the state of the device.""" return self.sensor.temperature / 100 - - -class HuePresence(GenericZLLSensor, BinarySensorDevice): - - _entity_id_format = BINARY_ENTITY_ID_FORMAT - device_class = 'presence' - icon = 'mdi:run' - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self.sensor.presence From 81602f0269e727a11689dbd62d2825fd46daf5d8 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Mon, 1 Apr 2019 11:03:11 +0100 Subject: [PATCH 06/28] Entity docstrings --- homeassistant/components/hue/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index d4ee448c7e9531..a25a0e86236b94 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -220,6 +220,7 @@ def device_info(self): class GenericZLLSensor(GenericHueSensor): + """Representation of a Hue-brand, physical sensor.""" @property def device_state_attributes(self): @@ -233,6 +234,7 @@ def device_state_attributes(self): class HuePresence(GenericZLLSensor, BinarySensorDevice): + """The presence sensor entity for a Hue motion sensor device.""" _entity_id_format = BINARY_ENTITY_ID_FORMAT device_class = 'presence' @@ -245,6 +247,7 @@ def is_on(self): class HueLightLevel(GenericZLLSensor, Entity): + """The light level sensor entity for a Hue motion sensor device.""" _entity_id_format = SENSOR_ENTITY_ID_FORMAT device_class = DEVICE_CLASS_ILLUMINANCE @@ -269,6 +272,7 @@ def device_state_attributes(self): class HueTemperature(GenericZLLSensor, Entity): + """The temperature sensor entity for a Hue motion sensor device.""" _entity_id_format = SENSOR_ENTITY_ID_FORMAT device_class = DEVICE_CLASS_TEMPERATURE From cf8e65514ea70ae24e4b055683c58852bc9d2304 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Mon, 1 Apr 2019 11:04:50 +0100 Subject: [PATCH 07/28] Remove lingering config for sensors --- homeassistant/components/hue/__init__.py | 4 +--- homeassistant/components/hue/bridge.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 363a03a10cbff1..beb29a55b27f18 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -97,14 +97,12 @@ async def async_setup_entry(hass, entry): if config is None: allow_unreachable = DEFAULT_ALLOW_UNREACHABLE allow_groups = DEFAULT_ALLOW_HUE_GROUPS - allow_sensors = DEFAULT_ALLOW_HUE_SENSORS else: allow_unreachable = config[CONF_ALLOW_UNREACHABLE] allow_groups = config[CONF_ALLOW_HUE_GROUPS] - allow_sensors = config[CONF_ALLOW_HUE_SENSORS] bridge = HueBridge( - hass, entry, allow_unreachable, allow_groups, allow_sensors) + hass, entry, allow_unreachable, allow_groups) if not await bridge.async_setup(): return False diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index f02ba55869fc5f..d645c022308ada 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -24,14 +24,12 @@ class HueBridge: """Manages a single Hue bridge.""" - def __init__(self, hass, config_entry, allow_unreachable, allow_groups, - allow_sensors): + def __init__(self, hass, config_entry, allow_unreachable, allow_groups): """Initialize the system.""" self.config_entry = config_entry self.hass = hass self.allow_unreachable = allow_unreachable self.allow_groups = allow_groups - self.allow_sensors = allow_sensors self.available = True self.api = None From ce8f2517f19c04010c1b18e80896aa977c2f6897 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Mon, 1 Apr 2019 11:05:45 +0100 Subject: [PATCH 08/28] Whitespace --- homeassistant/components/hue/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index a25a0e86236b94..b189462a18e8d6 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -212,8 +212,9 @@ def device_info(self): }, 'name': self.primary_sensor.name, 'manufacturer': self.primary_sensor.manufacturername, - 'model': self.primary_sensor.productname or \ - self.primary_sensor.modelid, + '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), } From 8c358aa5c8ccce7cad13a1c4bc54c5adb26efc1a Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Mon, 1 Apr 2019 23:21:44 +0100 Subject: [PATCH 09/28] Remove redundant entity ID generation and hass assignment. --- homeassistant/components/hue/sensor.py | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index b189462a18e8d6..9435de90ffe75b 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -7,13 +7,10 @@ import async_timeout from homeassistant.components import hue -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, ENTITY_ID_FORMAT as BINARY_ENTITY_ID_FORMAT) -from homeassistant.components.sensor import ( - ENTITY_ID_FORMAT as SENSOR_ENTITY_ID_FORMAT) +from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) -from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow @@ -98,7 +95,7 @@ async def async_update_items(hass, bridge, async_add_entities, current): name = PRESENCE_NAME_FORMAT.format(api[item_id].name) if api[item_id].type == aiohue.sensors.TYPE_ZLL_PRESENCE: - s = HuePresence(hass, api[item_id], name, bridge) + sensor = HuePresence(api[item_id], name, bridge) sensor_device_names[s.device_id] = api[item_id] current[item_id] = s new_sensors.append(s) @@ -121,16 +118,14 @@ async def async_update_items(hass, bridge, async_add_entities, current): name = LIGHT_LEVEL_NAME_FORMAT.format( primary_sensor.name) current[item_id] = HueLightLevel( - hass, api[item_id], name, bridge, - primary_sensor=primary_sensor) + api[item_id], name, bridge, primary_sensor=primary_sensor) elif api[item_id].type == aiohue.sensors.TYPE_ZLL_TEMPERATURE: if device_id in sensor_device_names: primary_sensor = sensor_device_names[device_id] name = TEMPERATURE_NAME_FORMAT.format( primary_sensor.name) current[item_id] = HueTemperature( - hass, api[item_id], name, bridge, - primary_sensor=primary_sensor) + api[item_id], name, bridge, primary_sensor=primary_sensor) else: continue @@ -145,10 +140,8 @@ class GenericHueSensor: should_poll = False - def __init__(self, hass, sensor, name, bridge, - primary_sensor=None): + def __init__(self, sensor, name, bridge, primary_sensor=None): """Initialize the sensor.""" - self.hass = hass self.sensor = sensor self._primary_sensor = primary_sensor self._name = name @@ -161,9 +154,6 @@ def __init__(self, hass, sensor, name, bridge, ) _LOGGER.warning(err, self.name) - self.entity_id = async_generate_entity_id( - self._entity_id_format, self.name, hass=hass) - @property def primary_sensor(self): """Return the entity which represents the primary sensor of @@ -237,7 +227,6 @@ def device_state_attributes(self): class HuePresence(GenericZLLSensor, BinarySensorDevice): """The presence sensor entity for a Hue motion sensor device.""" - _entity_id_format = BINARY_ENTITY_ID_FORMAT device_class = 'presence' icon = 'mdi:run' @@ -250,7 +239,6 @@ def is_on(self): class HueLightLevel(GenericZLLSensor, Entity): """The light level sensor entity for a Hue motion sensor device.""" - _entity_id_format = SENSOR_ENTITY_ID_FORMAT device_class = DEVICE_CLASS_ILLUMINANCE unit_of_measurement = "Lux" @@ -275,7 +263,6 @@ def device_state_attributes(self): class HueTemperature(GenericZLLSensor, Entity): """The temperature sensor entity for a Hue motion sensor device.""" - _entity_id_format = SENSOR_ENTITY_ID_FORMAT device_class = DEVICE_CLASS_TEMPERATURE unit_of_measurement = TEMP_CELSIUS From ab83b54c5812c58969f26e5c9c28db8e2070a858 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Mon, 1 Apr 2019 23:22:01 +0100 Subject: [PATCH 10/28] More meaningful variable name. --- homeassistant/components/hue/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 9435de90ffe75b..97376eafa5d114 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -96,9 +96,9 @@ async def async_update_items(hass, bridge, async_add_entities, current): name = PRESENCE_NAME_FORMAT.format(api[item_id].name) if api[item_id].type == aiohue.sensors.TYPE_ZLL_PRESENCE: sensor = HuePresence(api[item_id], name, bridge) - sensor_device_names[s.device_id] = api[item_id] - current[item_id] = s - new_sensors.append(s) + sensor_device_names[sensor.device_id] = api[item_id] + current[item_id] = sensor + new_sensors.append(sensor) # Iterate again now we have all the presence sensors, and add the related # sensors with nice names. From d1a13fba9d1ae0143fa3d973e1ce775c739743f4 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Mon, 1 Apr 2019 23:22:31 +0100 Subject: [PATCH 11/28] Add new 'not-darkness' pseudo-sensor. --- homeassistant/components/hue/sensor.py | 43 ++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 97376eafa5d114..c8c1f910628e87 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -20,6 +20,7 @@ PRESENCE_NAME_FORMAT = "{} presence" LIGHT_LEVEL_NAME_FORMAT = "{} light level" +IS_DARK_NAME_FORMAT = "{} is not dark" TEMPERATURE_NAME_FORMAT = "{} temperature" _LOGGER = logging.getLogger(__name__) @@ -113,12 +114,19 @@ async def async_update_items(hass, bridge, async_add_entities, current): name = api[item_id].name primary_sensor = None if api[item_id].type == aiohue.sensors.TYPE_ZLL_LIGHTLEVEL: + darkness_name = name + ' is not dark' if device_id in sensor_device_names: primary_sensor = sensor_device_names[device_id] name = LIGHT_LEVEL_NAME_FORMAT.format( primary_sensor.name) + darkness_name = IS_DARK_NAME_FORMAT.format( + primary_sensor.name) current[item_id] = HueLightLevel( api[item_id], name, bridge, primary_sensor=primary_sensor) + darkness = HueNotDarkness( + api[item_id], darkness_name, bridge, + primary_sensor=primary_sensor) + new_sensors.append(darkness) elif api[item_id].type == aiohue.sensors.TYPE_ZLL_TEMPERATURE: if device_id in sensor_device_names: primary_sensor = sensor_device_names[device_id] @@ -143,8 +151,8 @@ class GenericHueSensor: def __init__(self, sensor, name, bridge, primary_sensor=None): """Initialize the sensor.""" self.sensor = sensor - self._primary_sensor = primary_sensor self._name = name + self._primary_sensor = primary_sensor self.bridge = bridge if self.swupdatestate == "readytoinstall": @@ -236,6 +244,37 @@ def is_on(self): return self.sensor.presence +class HueNotDarkness(GenericZLLSensor, BinarySensorDevice): + """A binary light sensor entity for a Hue motion sensor device.""" + + device_class = 'light' + + @property + def is_on(self): + """Return the state of the device.""" + return not self.sensor.dark + + @property + def unique_id(self): + """Return the ID of this Hue sensor.""" + return self.sensor.uniqueid + '-not-dark' + + @property + def icon(self): + """Return an icon representing the entity and its state.""" + return self.is_on and 'mdi:lightbulb-on' or 'mdi:lightbulb-off' + + @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 HueLightLevel(GenericZLLSensor, Entity): """The light level sensor entity for a Hue motion sensor device.""" @@ -252,8 +291,6 @@ def device_state_attributes(self): """Return the device state attributes.""" attributes = super().device_state_attributes attributes.update({ - "is_dark": self.sensor.dark, - "is_daylight": self.sensor.daylight, "threshold_dark": self.sensor.tholddark, "threshold_offset": self.sensor.tholdoffset, }) From a32a84cd26bce248a3adf879af1a54367dc45e3b Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Mon, 1 Apr 2019 23:52:26 +0100 Subject: [PATCH 12/28] Refactor sensors into separate binary, non-binary, and shared modules. --- homeassistant/components/hue/binary_sensor.py | 51 ++++ homeassistant/components/hue/bridge.py | 2 + homeassistant/components/hue/hue_sensor.py | 236 +++++++++++++++ homeassistant/components/hue/sensor.py | 274 +----------------- 4 files changed, 292 insertions(+), 271 deletions(-) create mode 100644 homeassistant/components/hue/binary_sensor.py create mode 100644 homeassistant/components/hue/hue_sensor.py diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py new file mode 100644 index 00000000000000..5ddceb92784af1 --- /dev/null +++ b/homeassistant/components/hue/binary_sensor.py @@ -0,0 +1,51 @@ +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.hue.hue_sensor import ( + GenericZLLSensor, async_setup_entry as shared_async_setup_entry) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + 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' + icon = 'mdi:run' + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self.sensor.presence + + +class HueNotDarkness(GenericZLLSensor, BinarySensorDevice): + """A binary light sensor entity for a Hue motion sensor device.""" + + device_class = 'light' + + @property + def is_on(self): + """Return the state of the device.""" + return not self.sensor.dark + + @property + def unique_id(self): + """Return the ID of this Hue sensor.""" + return self.sensor.uniqueid + '-not-dark' + + @property + def icon(self): + """Return an icon representing the entity and its state.""" + return self.is_on and 'mdi:lightbulb-on' or 'mdi:lightbulb-off' + + @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 diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index d645c022308ada..2ec1cf48426239 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -69,6 +69,8 @@ async def async_setup(self, tries=0): 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')) diff --git a/homeassistant/components/hue/hue_sensor.py b/homeassistant/components/hue/hue_sensor.py new file mode 100644 index 00000000000000..adec959eb2e479 --- /dev/null +++ b/homeassistant/components/hue/hue_sensor.py @@ -0,0 +1,236 @@ +"""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.helpers.event import async_track_point_in_utc_time +from homeassistant.util.dt import utcnow + + +DEPENDENCIES = ['hue'] +SCAN_INTERVAL = timedelta(seconds=5) + +PRESENCE_NAME_FORMAT = "{} presence" +LIGHT_LEVEL_NAME_FORMAT = "{} light level" +IS_DARK_NAME_FORMAT = "{} is not dark" +TEMPERATURE_NAME_FORMAT = "{} temperature" + +_LOGGER = logging.getLogger(__name__) + + +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']] + cur_sensors = {} + + async def async_update_bridge(now): + """Update the values of the bridge. + + Will update sensors from the bridge. + """ + + await async_update_items( + hass, bridge, async_add_entities, cur_sensors, binary=binary) + + async_track_point_in_utc_time( + hass, async_update_bridge, utcnow() + SCAN_INTERVAL) + + await async_update_bridge(None) + + +async def async_update_items(hass, bridge, async_add_entities, current, + binary=False): + """Update sensors from the bridge.""" + import aiohue + from homeassistant.components.hue.binary_sensor import ( + HueNotDarkness, HuePresence) + from homeassistant.components.hue.sensor import ( + HueLightLevel, HueTemperature) + + api = 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 bridge.available: + return + + _LOGGER.error('Unable to reach bridge %s (%s)', bridge.host, err) + bridge.available = False + + return + + finally: + _LOGGER.debug('Finished sensor request in %.3f seconds', + monotonic() - start) + + if not bridge.available: + _LOGGER.info('Reconnected to bridge %s', bridge.host) + bridge.available = True + + new_sensors = [] + sensor_device_names = {} + + # 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 item_id in current: + continue + + name = PRESENCE_NAME_FORMAT.format(api[item_id].name) + if api[item_id].type == aiohue.sensors.TYPE_ZLL_PRESENCE: + sensor = HuePresence(api[item_id], name, bridge) + sensor_device_names[sensor.device_id] = api[item_id] + current[item_id] = sensor + if binary: + new_sensors.append(sensor) + + # Iterate again now we have all the presence sensors, and add the related + # sensors with nice names. + for item_id in api: + if item_id in current: + continue + + # Work out the shared device ID, as described above + device_id = api[item_id].uniqueid + if device_id and len(device_id) > 23: + device_id = device_id[:23] + name = api[item_id].name + primary_sensor = None + if api[item_id].type == aiohue.sensors.TYPE_ZLL_LIGHTLEVEL: + darkness_name = name + ' is not dark' + if device_id in sensor_device_names: + primary_sensor = sensor_device_names[device_id] + name = LIGHT_LEVEL_NAME_FORMAT.format( + primary_sensor.name) + darkness_name = IS_DARK_NAME_FORMAT.format( + primary_sensor.name) + current[item_id] = HueLightLevel( + api[item_id], name, bridge, primary_sensor=primary_sensor) + if binary: + darkness = HueNotDarkness( + api[item_id], darkness_name, bridge, + primary_sensor=primary_sensor) + new_sensors.append(darkness) + elif api[item_id].type == aiohue.sensors.TYPE_ZLL_TEMPERATURE: + if device_id in sensor_device_names: + primary_sensor = sensor_device_names[device_id] + name = TEMPERATURE_NAME_FORMAT.format( + primary_sensor.name) + current[item_id] = HueTemperature( + api[item_id], name, bridge, primary_sensor=primary_sensor) + else: + continue + + if not binary: + new_sensors.append(current[item_id]) + + if new_sensors: + async_add_entities(new_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 + + if self.swupdatestate == "readytoinstall": + err = ( + "Please check for software updates of the %s " + "sensor in the Philips Hue App." + ) + _LOGGER.warning(err, self.name) + + @property + def primary_sensor(self): + """Return the entity which represents the primary sensor of + this device. + """ + + return self._primary_sensor or self.sensor + + @property + def device_id(self): + """Return the ID that represents 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): + """The state of available software updates for this device.""" + return self.primary_sensor.raw.get('swupdate', {}).get('state') + + @property + def device_info(self): + """Return the device info to link 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": self.sensor.battery, + "last_updated": self.sensor.lastupdated, + "on": self.sensor.on, + "reachable": self.sensor.reachable, + } diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index c8c1f910628e87..e53722df50d511 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -1,278 +1,10 @@ -"""Support for the Philips Hue sensors.""" -import asyncio -from datetime import timedelta -import logging -from time import monotonic - -import async_timeout - -from homeassistant.components import hue -from homeassistant.components.binary_sensor import BinarySensorDevice +"""Hue sensor entities.""" from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.util.dt import utcnow - - -DEPENDENCIES = ['hue'] -SCAN_INTERVAL = timedelta(seconds=5) - -PRESENCE_NAME_FORMAT = "{} presence" -LIGHT_LEVEL_NAME_FORMAT = "{} light level" -IS_DARK_NAME_FORMAT = "{} is not dark" -TEMPERATURE_NAME_FORMAT = "{} temperature" - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the Hue sensors from a config entry.""" - bridge = hass.data[hue.DOMAIN][config_entry.data['host']] - cur_sensors = {} - - async def async_update_bridge(now): - """Update the values of the bridge. - - Will update sensors from the bridge. - """ - - await async_update_items( - hass, bridge, async_add_entities, cur_sensors) - - async_track_point_in_utc_time( - hass, async_update_bridge, utcnow() + SCAN_INTERVAL) - - await async_update_bridge(None) - - -async def async_update_items(hass, bridge, async_add_entities, current): - """Update sensors from the bridge.""" - import aiohue - - api = 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 bridge.available: - return - - _LOGGER.error('Unable to reach bridge %s (%s)', bridge.host, err) - bridge.available = False - - return - - finally: - _LOGGER.debug('Finished sensor request in %.3f seconds', - monotonic() - start) - - if not bridge.available: - _LOGGER.info('Reconnected to bridge %s', bridge.host) - bridge.available = True - - new_sensors = [] - sensor_device_names = {} - - # 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 item_id in current: - continue - - name = PRESENCE_NAME_FORMAT.format(api[item_id].name) - if api[item_id].type == aiohue.sensors.TYPE_ZLL_PRESENCE: - sensor = HuePresence(api[item_id], name, bridge) - sensor_device_names[sensor.device_id] = api[item_id] - current[item_id] = sensor - new_sensors.append(sensor) - - # Iterate again now we have all the presence sensors, and add the related - # sensors with nice names. - for item_id in api: - if item_id in current: - continue - - # Work out the shared device ID, as described above - device_id = api[item_id].uniqueid - if device_id and len(device_id) > 23: - device_id = device_id[:23] - name = api[item_id].name - primary_sensor = None - if api[item_id].type == aiohue.sensors.TYPE_ZLL_LIGHTLEVEL: - darkness_name = name + ' is not dark' - if device_id in sensor_device_names: - primary_sensor = sensor_device_names[device_id] - name = LIGHT_LEVEL_NAME_FORMAT.format( - primary_sensor.name) - darkness_name = IS_DARK_NAME_FORMAT.format( - primary_sensor.name) - current[item_id] = HueLightLevel( - api[item_id], name, bridge, primary_sensor=primary_sensor) - darkness = HueNotDarkness( - api[item_id], darkness_name, bridge, - primary_sensor=primary_sensor) - new_sensors.append(darkness) - elif api[item_id].type == aiohue.sensors.TYPE_ZLL_TEMPERATURE: - if device_id in sensor_device_names: - primary_sensor = sensor_device_names[device_id] - name = TEMPERATURE_NAME_FORMAT.format( - primary_sensor.name) - current[item_id] = HueTemperature( - api[item_id], name, bridge, primary_sensor=primary_sensor) - else: - continue - - new_sensors.append(current[item_id]) - - if new_sensors: - async_add_entities(new_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 - - if self.swupdatestate == "readytoinstall": - err = ( - "Please check for software updates of the %s " - "sensor in the Philips Hue App." - ) - _LOGGER.warning(err, self.name) - - @property - def primary_sensor(self): - """Return the entity which represents the primary sensor of - this device. - """ - - return self._primary_sensor or self.sensor - - @property - def device_id(self): - """Return the ID that represents 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): - """The state of available software updates for this device.""" - return self.primary_sensor.raw.get('swupdate', {}).get('state') - - @property - def device_info(self): - """Return the device info to link 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": self.sensor.battery, - "last_updated": self.sensor.lastupdated, - "on": self.sensor.on, - "reachable": self.sensor.reachable, - } - - -class HuePresence(GenericZLLSensor, BinarySensorDevice): - """The presence sensor entity for a Hue motion sensor device.""" - - device_class = 'presence' - icon = 'mdi:run' - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self.sensor.presence - - -class HueNotDarkness(GenericZLLSensor, BinarySensorDevice): - """A binary light sensor entity for a Hue motion sensor device.""" - - device_class = 'light' - - @property - def is_on(self): - """Return the state of the device.""" - return not self.sensor.dark - - @property - def unique_id(self): - """Return the ID of this Hue sensor.""" - return self.sensor.uniqueid + '-not-dark' - - @property - def icon(self): - """Return an icon representing the entity and its state.""" - return self.is_on and 'mdi:lightbulb-on' or 'mdi:lightbulb-off' +from homeassistant.components.hue.hue_sensor import ( + GenericZLLSensor, async_setup_entry) - @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 HueLightLevel(GenericZLLSensor, Entity): From 3c87828c8ca9585cfd32f7fab2b71187beab183b Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Mon, 1 Apr 2019 23:56:10 +0100 Subject: [PATCH 13/28] formatting --- homeassistant/components/hue/hue_sensor.py | 3 ++- homeassistant/components/hue/sensor.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hue/hue_sensor.py b/homeassistant/components/hue/hue_sensor.py index adec959eb2e479..5413579fb36a7b 100644 --- a/homeassistant/components/hue/hue_sensor.py +++ b/homeassistant/components/hue/hue_sensor.py @@ -22,7 +22,8 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, config_entry, async_add_entities, binary=False): +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']] cur_sensors = {} diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index e53722df50d511..c31e6f0a5d7fea 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -6,6 +6,9 @@ GenericZLLSensor, async_setup_entry) +# No-op to trick static code analysis tools. +async_setup_entry = async_setup_entry + class HueLightLevel(GenericZLLSensor, Entity): """The light level sensor entity for a Hue motion sensor device.""" From 5ca133db3f2b3b0077a0960b5a55c95f0be2eefb Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Tue, 2 Apr 2019 10:17:18 +0100 Subject: [PATCH 14/28] make linter happy. --- homeassistant/components/hue/sensor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index c31e6f0a5d7fea..575b7e5894ce4f 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -3,11 +3,12 @@ DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) from homeassistant.helpers.entity import Entity from homeassistant.components.hue.hue_sensor import ( - GenericZLLSensor, async_setup_entry) + GenericZLLSensor, async_setup_entry as shared_async_setup_entry) -# No-op to trick static code analysis tools. -async_setup_entry = async_setup_entry +async def async_setup_entry(hass, config_entry, async_add_entities): + await shared_async_setup_entry( + hass, config_entry, async_add_entities) class HueLightLevel(GenericZLLSensor, Entity): From 38d3b3dd3933601016a17713c5a345d83c1e9e6b Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Sun, 7 Apr 2019 13:35:39 +0100 Subject: [PATCH 15/28] Refactor again, fix update mechanism, and address comments. --- .coveragerc | 1 - homeassistant/components/hue/binary_sensor.py | 35 +---- homeassistant/components/hue/sensor.py | 5 +- .../hue/{hue_sensor.py => sensor_base.py} | 124 +++++++++--------- tests/components/hue/test_bridge.py | 14 +- 5 files changed, 76 insertions(+), 103 deletions(-) rename homeassistant/components/hue/{hue_sensor.py => sensor_base.py} (66%) diff --git a/.coveragerc b/.coveragerc index 411e9aac5990f0..86819ef51a39b8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -262,7 +262,6 @@ omit = homeassistant/components/huawei_lte/* homeassistant/components/huawei_router/device_tracker.py homeassistant/components/hue/light.py - homeassistant/components/hue/sensor.py homeassistant/components/hunterdouglas_powerview/scene.py homeassistant/components/hydrawise/* homeassistant/components/hyperion/light.py diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index 5ddceb92784af1..38ddc155c1835d 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -1,9 +1,10 @@ from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.hue.hue_sensor import ( +from homeassistant.components.hue.sensor_base import ( GenericZLLSensor, async_setup_entry as shared_async_setup_entry) 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) @@ -12,40 +13,8 @@ class HuePresence(GenericZLLSensor, BinarySensorDevice): """The presence sensor entity for a Hue motion sensor device.""" device_class = 'presence' - icon = 'mdi:run' @property def is_on(self): """Return true if the binary sensor is on.""" return self.sensor.presence - - -class HueNotDarkness(GenericZLLSensor, BinarySensorDevice): - """A binary light sensor entity for a Hue motion sensor device.""" - - device_class = 'light' - - @property - def is_on(self): - """Return the state of the device.""" - return not self.sensor.dark - - @property - def unique_id(self): - """Return the ID of this Hue sensor.""" - return self.sensor.uniqueid + '-not-dark' - - @property - def icon(self): - """Return an icon representing the entity and its state.""" - return self.is_on and 'mdi:lightbulb-on' or 'mdi:lightbulb-off' - - @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 diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 575b7e5894ce4f..c5b3a039e21d1f 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -2,13 +2,14 @@ from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) from homeassistant.helpers.entity import Entity -from homeassistant.components.hue.hue_sensor import ( +from homeassistant.components.hue.sensor_base import ( GenericZLLSensor, async_setup_entry as shared_async_setup_entry) 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) + hass, config_entry, async_add_entities, binary=False) class HueLightLevel(GenericZLLSensor, Entity): diff --git a/homeassistant/components/hue/hue_sensor.py b/homeassistant/components/hue/sensor_base.py similarity index 66% rename from homeassistant/components/hue/hue_sensor.py rename to homeassistant/components/hue/sensor_base.py index 5413579fb36a7b..8aa8978200d0ca 100644 --- a/homeassistant/components/hue/hue_sensor.py +++ b/homeassistant/components/hue/sensor_base.py @@ -11,22 +11,55 @@ from homeassistant.util.dt import utcnow -DEPENDENCIES = ['hue'] SCAN_INTERVAL = timedelta(seconds=5) +CURRENT_SENSORS = 'current_sensors' PRESENCE_NAME_FORMAT = "{} presence" LIGHT_LEVEL_NAME_FORMAT = "{} light level" -IS_DARK_NAME_FORMAT = "{} is not dark" TEMPERATURE_NAME_FORMAT = "{} temperature" + +_HUE_SENSOR_TYPE_CONFIG_MAP = {} + _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.""" + import aiohue + from homeassistant.components.hue.binary_sensor import HuePresence + from homeassistant.components.hue.sensor import ( + HueLightLevel, HueTemperature) + + _HUE_SENSOR_TYPE_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, + }, + }) + bridge = hass.data[hue.DOMAIN][config_entry.data['host']] - cur_sensors = {} + cur_sensors = hass.data[hue.DOMAIN][CURRENT_SENSORS] = {} async def async_update_bridge(now): """Update the values of the bridge. @@ -35,7 +68,7 @@ async def async_update_bridge(now): """ await async_update_items( - hass, bridge, async_add_entities, cur_sensors, binary=binary) + hass, bridge, async_add_entities, binary=binary) async_track_point_in_utc_time( hass, async_update_bridge, utcnow() + SCAN_INTERVAL) @@ -43,14 +76,9 @@ async def async_update_bridge(now): await async_update_bridge(None) -async def async_update_items(hass, bridge, async_add_entities, current, - binary=False): +async def async_update_items(hass, bridge, async_add_entities, binary=False): """Update sensors from the bridge.""" import aiohue - from homeassistant.components.hue.binary_sensor import ( - HueNotDarkness, HuePresence) - from homeassistant.components.hue.sensor import ( - HueLightLevel, HueTemperature) api = bridge.api.sensors @@ -78,7 +106,8 @@ async def async_update_items(hass, bridge, async_add_entities, current, bridge.available = True new_sensors = [] - sensor_device_names = {} + primary_sensor_devices = {} + current = 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 @@ -93,56 +122,37 @@ async def async_update_items(hass, bridge, async_add_entities, current, # 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 item_id in current: + if api[item_id].type != aiohue.sensors.TYPE_ZLL_PRESENCE: continue - name = PRESENCE_NAME_FORMAT.format(api[item_id].name) - if api[item_id].type == aiohue.sensors.TYPE_ZLL_PRESENCE: - sensor = HuePresence(api[item_id], name, bridge) - sensor_device_names[sensor.device_id] = api[item_id] - current[item_id] = sensor - if binary: - new_sensors.append(sensor) + 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. + # sensors with nice names where appropriate. for item_id in api: - if item_id in current: + existing = current.get(api[item_id].uniqueid) + if existing is not None: + existing.sensor = api[item_id] + existing.async_schedule_update_ha_state() continue - # Work out the shared device ID, as described above - device_id = api[item_id].uniqueid - if device_id and len(device_id) > 23: - device_id = device_id[:23] - name = api[item_id].name primary_sensor = None - if api[item_id].type == aiohue.sensors.TYPE_ZLL_LIGHTLEVEL: - darkness_name = name + ' is not dark' - if device_id in sensor_device_names: - primary_sensor = sensor_device_names[device_id] - name = LIGHT_LEVEL_NAME_FORMAT.format( - primary_sensor.name) - darkness_name = IS_DARK_NAME_FORMAT.format( - primary_sensor.name) - current[item_id] = HueLightLevel( - api[item_id], name, bridge, primary_sensor=primary_sensor) - if binary: - darkness = HueNotDarkness( - api[item_id], darkness_name, bridge, - primary_sensor=primary_sensor) - new_sensors.append(darkness) - elif api[item_id].type == aiohue.sensors.TYPE_ZLL_TEMPERATURE: - if device_id in sensor_device_names: - primary_sensor = sensor_device_names[device_id] - name = TEMPERATURE_NAME_FORMAT.format( - primary_sensor.name) - current[item_id] = HueTemperature( - api[item_id], name, bridge, primary_sensor=primary_sensor) - else: + sensor_config = _HUE_SENSOR_TYPE_CONFIG_MAP.get(api[item_id].type) + if sensor_config is None: continue - if not binary: - new_sensors.append(current[item_id]) + if binary != sensor_config["binary"]: + 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, bridge, primary_sensor=primary_sensor) + new_sensors.append(current[api[item_id].uniqueid]) if new_sensors: async_add_entities(new_sensors) @@ -160,13 +170,6 @@ def __init__(self, sensor, name, bridge, primary_sensor=None): self._primary_sensor = primary_sensor self.bridge = bridge - if self.swupdatestate == "readytoinstall": - err = ( - "Please check for software updates of the %s " - "sensor in the Philips Hue App." - ) - _LOGGER.warning(err, self.name) - @property def primary_sensor(self): """Return the entity which represents the primary sensor of @@ -230,8 +233,5 @@ class GenericZLLSensor(GenericHueSensor): def device_state_attributes(self): """Return the device state attributes.""" return { - "battery": self.sensor.battery, - "last_updated": self.sensor.lastupdated, - "on": self.sensor.on, - "reachable": self.sensor.reachable, + "battery_level": self.sensor.battery } diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 855a12e26208fb..1e62c34b29f292 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -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] + 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 From 0f02e71cebb6ed092ad49e5fc9e5728a80eca3a5 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Sun, 7 Apr 2019 15:05:15 +0100 Subject: [PATCH 16/28] Remove unnecessary assignment --- homeassistant/components/hue/sensor_base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 8aa8978200d0ca..daa24e8ecf8ddc 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -132,7 +132,6 @@ async def async_update_items(hass, bridge, async_add_entities, binary=False): for item_id in api: existing = current.get(api[item_id].uniqueid) if existing is not None: - existing.sensor = api[item_id] existing.async_schedule_update_ha_state() continue From 5e1584ba36cc13eae8b7cff34e8dea3572d5bbac Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Sun, 7 Apr 2019 17:07:15 +0100 Subject: [PATCH 17/28] Small fixes. --- homeassistant/components/hue/binary_sensor.py | 1 + homeassistant/components/hue/sensor_base.py | 21 +++++++------------ tests/components/hue/test_bridge.py | 2 +- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index 38ddc155c1835d..baf5a541c6a799 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -1,3 +1,4 @@ +"""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) diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index daa24e8ecf8ddc..22ec095969515a 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -59,14 +59,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities, }) bridge = hass.data[hue.DOMAIN][config_entry.data['host']] - cur_sensors = hass.data[hue.DOMAIN][CURRENT_SENSORS] = {} + hass.data[hue.DOMAIN][CURRENT_SENSORS] = {} async def async_update_bridge(now): - """Update the values of the bridge. - - Will update sensors from the bridge. - """ - + """Will update sensors from the bridge.""" await async_update_items( hass, bridge, async_add_entities, binary=binary) @@ -174,14 +170,11 @@ def primary_sensor(self): """Return the entity which represents the primary sensor of this device. """ - return self._primary_sensor or self.sensor @property def device_id(self): - """Return the ID that represents the physical device this - sensor is part of. - """ + """Return the ID of the physical device this sensor is part of.""" return self.unique_id[:23] @property @@ -202,15 +195,15 @@ def available(self): @property def swupdatestate(self): - """The state of available software updates for this device.""" + """Return detail of available software updates for this device.""" return self.primary_sensor.raw.get('swupdate', {}).get('state') @property def device_info(self): - """Return the device info to link individual entities together - in the hass device registry. - """ + """Return the device info. + Links individual entities together in the hass device registry. + """ return { 'identifiers': { (hue.DOMAIN, self.device_id) diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 1e62c34b29f292..5798bf4b650ea9 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -27,7 +27,7 @@ async def test_bridge_setup(): 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'] + assert forward_entries == set(['light', 'binary_sensor', 'sensor']) async def test_bridge_setup_invalid_username(): From a2aa94697117cdc28d942fa4093d9a9205f82155 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Sun, 7 Apr 2019 20:45:55 +0100 Subject: [PATCH 18/28] docstring --- homeassistant/components/hue/sensor_base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 22ec095969515a..2c160226762687 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -167,9 +167,7 @@ def __init__(self, sensor, name, bridge, primary_sensor=None): @property def primary_sensor(self): - """Return the entity which represents the primary sensor of - this device. - """ + """Return the primary sensor entity of the physical device.""" return self._primary_sensor or self.sensor @property From ac80fc097e1cb30071f57a4491196602db8a75ec Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Wed, 10 Apr 2019 20:01:10 +0100 Subject: [PATCH 19/28] Another refactor: only call API once and make testing easier --- homeassistant/components/hue/bridge.py | 9 +- homeassistant/components/hue/sensor_base.py | 255 +++++++++++--------- 2 files changed, 154 insertions(+), 110 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 2ec1cf48426239..80fbbb8c07ed69 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -98,8 +98,13 @@ async def async_reset(self): # 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') + light = (await self.hass.config_entries.async_forward_entry_unload( + self.config_entry, 'light')) is not False + binary = (await self.hass.config_entries.async_forward_entry_unload( + self.config_entry, 'binary_sensor')) is not False + sensor = (await self.hass.config_entries.async_forward_entry_unload( + self.config_entry, 'sensor')) is not False + return light and binary and sensor async def hue_activate_scene(self, call, updated=False): """Service to call directly into bridge to set scenes.""" diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 2c160226762687..a02fa2b7568edf 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -11,18 +11,18 @@ from homeassistant.util.dt import utcnow -SCAN_INTERVAL = timedelta(seconds=5) CURRENT_SENSORS = 'current_sensors' +SENSOR_MANAGER = 'sensor_manager' PRESENCE_NAME_FORMAT = "{} presence" LIGHT_LEVEL_NAME_FORMAT = "{} light level" TEMPERATURE_NAME_FORMAT = "{} temperature" - -_HUE_SENSOR_TYPE_CONFIG_MAP = {} - _LOGGER = logging.getLogger(__name__) +# Used for testing +_ASYNC_UPDATE_BRIDGE = None + def _device_id(aiohue_sensor): # Work out the shared device ID, as described below @@ -35,122 +35,161 @@ def _device_id(aiohue_sensor): async def async_setup_entry(hass, config_entry, async_add_entities, binary=False): """Set up the Hue sensors from a config entry.""" - import aiohue - from homeassistant.components.hue.binary_sensor import HuePresence - from homeassistant.components.hue.sensor import ( - HueLightLevel, HueTemperature) - - _HUE_SENSOR_TYPE_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, - }, - }) bridge = hass.data[hue.DOMAIN][config_entry.data['host']] - hass.data[hue.DOMAIN][CURRENT_SENSORS] = {} + 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: + + SCAN_INTERVAL = timedelta(seconds=5) + sensor_config_map = {} + + def __init__(self, hass, bridge): + import aiohue + from homeassistant.components.hue.binary_sensor import HuePresence + from homeassistant.components.hue.sensor import ( + HueLightLevel, HueTemperature) + + 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): + 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 def async_update_bridge(now): - """Will update sensors from the bridge.""" - await async_update_items( - hass, bridge, async_add_entities, binary=binary) + async_track_point_in_utc_time( + self.hass, async_update_bridge, utcnow() + self.SCAN_INTERVAL) - async_track_point_in_utc_time( - hass, async_update_bridge, utcnow() + SCAN_INTERVAL) + await async_update_bridge(None) - await async_update_bridge(None) + async def async_update_items(self): + """Update sensors from the bridge.""" + import aiohue + api = self.bridge.api.sensors -async def async_update_items(hass, bridge, async_add_entities, binary=False): - """Update sensors from the bridge.""" - import aiohue + 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) - api = bridge.api.sensors + if not self.bridge.available: + return - 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) + _LOGGER.error('Unable to reach bridge %s (%s)', self.bridge.host, err) + self.bridge.available = False - if not bridge.available: return - _LOGGER.error('Unable to reach bridge %s (%s)', bridge.host, err) - bridge.available = False - - return - - finally: - _LOGGER.debug('Finished sensor request in %.3f seconds', - monotonic() - start) - - if not bridge.available: - _LOGGER.info('Reconnected to bridge %s', bridge.host) - bridge.available = True - - new_sensors = [] - primary_sensor_devices = {} - current = 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: - existing.async_schedule_update_ha_state() - continue - - primary_sensor = None - sensor_config = _HUE_SENSOR_TYPE_CONFIG_MAP.get(api[item_id].type) - if sensor_config is None: - continue - - if binary != sensor_config["binary"]: - 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, bridge, primary_sensor=primary_sensor) - new_sensors.append(current[api[item_id].uniqueid]) - - if new_sensors: - async_add_entities(new_sensors) + 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: + if existing.hass is not None: + existing.async_schedule_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: From 4ba490e9ec27c5cc3839f20dfbf7f689ca1e3993 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Wed, 10 Apr 2019 20:01:22 +0100 Subject: [PATCH 20/28] Tests & test fixes --- tests/components/hue/test_bridge.py | 2 +- tests/components/hue/test_sensor_base.py | 430 +++++++++++++++++++++++ 2 files changed, 431 insertions(+), 1 deletion(-) create mode 100644 tests/components/hue/test_sensor_base.py diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 5798bf4b650ea9..5b383afc53dbe5 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -22,7 +22,7 @@ async def test_bridge_setup(): assert hue_bridge.api is api forward_entries = set( - c[1] + c[1][1] for c in hass.config_entries.async_forward_entry_setup.mock_calls ) diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py new file mode 100644 index 00000000000000..ab0a5dcbbdd605 --- /dev/null +++ b/tests/components/hue/test_sensor_base.py @@ -0,0 +1,430 @@ +"""Philips Hue sensors platform tests.""" +import asyncio +from collections import deque +import logging +from unittest.mock import Mock + +import aiohue +from aiohue.sensors import ( + Sensors, ZLLLightLevelSensor, ZLLPresenceSensor, ZLLTemperatureSensor) +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 + } +} +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_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 + await hass.data[hue.DOMAIN][hue_sensor_base.SENSOR_MANAGER].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' From 4b5bb8df8120a97afc96d22fdeaf4fac51f714f1 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Wed, 10 Apr 2019 20:22:20 +0100 Subject: [PATCH 21/28] Flake & lint --- homeassistant/components/hue/sensor_base.py | 42 +- tests/components/hue/test_sensor_base.py | 555 ++++++++++---------- 2 files changed, 306 insertions(+), 291 deletions(-) diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index a02fa2b7568edf..4992852d5f55f1 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -35,7 +35,6 @@ def _device_id(aiohue_sensor): 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, {}) @@ -44,16 +43,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities, manager = SensorManager(hass, bridge) hass.data[hue.DOMAIN][SENSOR_MANAGER] = manager - manager._register_component(binary, async_add_entities) + 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 homeassistant.components.hue.binary_sensor import HuePresence from homeassistant.components.hue.sensor import ( @@ -82,11 +86,12 @@ def __init__(self, hass, bridge): }, }) - def _register_component(self, binary, async_add_entities): + 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""" + """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: @@ -121,7 +126,8 @@ async def async_update_items(self): if not self.bridge.available: return - _LOGGER.error('Unable to reach bridge %s (%s)', self.bridge.host, err) + _LOGGER.error('Unable to reach bridge %s (%s)', self.bridge.host, + err) self.bridge.available = False return @@ -141,24 +147,25 @@ async def async_update_items(self): # 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). + # 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. + # 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. + # 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: @@ -172,7 +179,8 @@ async def async_update_items(self): continue base_name = api[item_id].name - primary_sensor = primary_sensor_devices.get(_device_id(api[item_id])) + 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) diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index ab0a5dcbbdd605..cef6e2531d0636 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -1,12 +1,10 @@ """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, ZLLLightLevelSensor, ZLLPresenceSensor, ZLLTemperatureSensor) +from aiohue.sensors import Sensors import pytest from homeassistant import config_entries @@ -16,283 +14,283 @@ _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 - } + "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 - } + "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 - } + "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 - } + "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 - } + "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 - } + "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 - } + "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 - } + "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 - } + "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 + } } SENSOR_RESPONSE = { "1": PRESENCE_SENSOR_1_PRESENT, @@ -348,8 +346,10 @@ async def setup_bridge(hass, 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') + 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() @@ -371,9 +371,12 @@ async def test_sensors(hass, mock_bridge): # 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') + 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 @@ -383,9 +386,12 @@ async def test_sensors(hass, mock_bridge): 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') + 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 @@ -414,7 +420,8 @@ async def test_new_sensor_discovered(hass, mock_bridge): mock_bridge.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - await hass.data[hue.DOMAIN][hue_sensor_base.SENSOR_MANAGER].async_update_items() + 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() From 41dff1955bce158c97ca61f0e8e34353075dc3ea Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Wed, 10 Apr 2019 22:04:28 +0100 Subject: [PATCH 22/28] Use gather and dispatcher --- homeassistant/components/hue/bridge.py | 17 ++++++++++------- homeassistant/components/hue/sensor_base.py | 3 +-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 80fbbb8c07ed69..25db031e6bf6ec 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -98,13 +98,16 @@ async def async_reset(self): # If setup was successful, we set api variable, forwarded entry and # register service - light = (await self.hass.config_entries.async_forward_entry_unload( - self.config_entry, 'light')) is not False - binary = (await self.hass.config_entries.async_forward_entry_unload( - self.config_entry, 'binary_sensor')) is not False - sensor = (await self.hass.config_entries.async_forward_entry_unload( - self.config_entry, 'sensor')) is not False - return light and binary and sensor + 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.""" diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 4992852d5f55f1..453e855ab5a1cf 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -169,8 +169,7 @@ async def async_update_items(self): for item_id in api: existing = current.get(api[item_id].uniqueid) if existing is not None: - if existing.hass is not None: - existing.async_schedule_update_ha_state() + self.hass.async_create_task(existing.async_update_ha_state()) continue primary_sensor = None From 5883e16ef5b3d0560aba344f3c868c7bcece3475 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Wed, 10 Apr 2019 22:08:06 +0100 Subject: [PATCH 23/28] Remove unnecessary whitespace change. --- homeassistant/components/hue/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index beb29a55b27f18..ac17e6e852f435 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -101,8 +101,7 @@ async def async_setup_entry(hass, entry): allow_unreachable = config[CONF_ALLOW_UNREACHABLE] allow_groups = config[CONF_ALLOW_HUE_GROUPS] - bridge = HueBridge( - hass, entry, allow_unreachable, allow_groups) + bridge = HueBridge(hass, entry, allow_unreachable, allow_groups) if not await bridge.async_setup(): return False From c6633fbdf94fe1efac9b0c8d8bf4f21930558c37 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Wed, 10 Apr 2019 22:17:14 +0100 Subject: [PATCH 24/28] Move component related stuff out of the shared module --- homeassistant/components/hue/binary_sensor.py | 3 +++ homeassistant/components/hue/sensor.py | 4 ++++ homeassistant/components/hue/sensor_base.py | 10 ++++------ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index baf5a541c6a799..e0a8e2d955c1b9 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -4,6 +4,9 @@ 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( diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index c5b3a039e21d1f..bd8fa6a5334bb3 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -6,6 +6,10 @@ 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( diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 453e855ab5a1cf..d25e5ea9c3528f 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -14,10 +14,6 @@ CURRENT_SENSORS = 'current_sensors' SENSOR_MANAGER = 'sensor_manager' -PRESENCE_NAME_FORMAT = "{} presence" -LIGHT_LEVEL_NAME_FORMAT = "{} light level" -TEMPERATURE_NAME_FORMAT = "{} temperature" - _LOGGER = logging.getLogger(__name__) # Used for testing @@ -59,9 +55,11 @@ class SensorManager: def __init__(self, hass, bridge): """Initialize the sensor manager.""" import aiohue - from homeassistant.components.hue.binary_sensor import HuePresence + from homeassistant.components.hue.binary_sensor import ( + HuePresence, PRESENCE_NAME_FORMAT) from homeassistant.components.hue.sensor import ( - HueLightLevel, HueTemperature) + HueLightLevel, HueTemperature, LIGHT_LEVEL_NAME_FORMAT, + TEMPERATURE_NAME_FORMAT) self.hass = hass self.bridge = bridge From 4ee15fcc9bb71e9df13507dd403feba10d717a5e Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Wed, 10 Apr 2019 22:17:28 +0100 Subject: [PATCH 25/28] Remove unused remnant of failed approach. --- homeassistant/components/hue/sensor_base.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index d25e5ea9c3528f..8de472e7894872 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -16,9 +16,6 @@ _LOGGER = logging.getLogger(__name__) -# Used for testing -_ASYNC_UPDATE_BRIDGE = None - def _device_id(aiohue_sensor): # Work out the shared device ID, as described below From f291a6223b3fbfba6967280fae0af13efb1f6723 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Fri, 12 Apr 2019 18:08:51 +0100 Subject: [PATCH 26/28] Increase test coverage --- tests/components/hue/test_sensor_base.py | 48 ++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index cef6e2531d0636..99829c59666d2e 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -1,9 +1,11 @@ """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 @@ -292,6 +294,23 @@ "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, @@ -402,6 +421,17 @@ async def test_sensors(hass, mock_bridge): 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) @@ -435,3 +465,21 @@ async def test_new_sensor_discovered(hass, mock_bridge): 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 From 74fb6f18b9e62e9be0c679728be8717b980fc922 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Sat, 13 Apr 2019 07:36:49 +0100 Subject: [PATCH 27/28] Don't get too upset if we're already trying to update an entity before it has finished adding --- homeassistant/components/hue/binary_sensor.py | 3 +++ homeassistant/components/hue/sensor.py | 11 +++++++++-- homeassistant/components/hue/sensor_base.py | 18 +++++++++++++++++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index e0a8e2d955c1b9..d60750721ac351 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -18,6 +18,9 @@ class HuePresence(GenericZLLSensor, BinarySensorDevice): 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.""" diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index bd8fa6a5334bb3..555c16a0be7d32 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -16,7 +16,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config_entry, async_add_entities, binary=False) -class HueLightLevel(GenericZLLSensor, Entity): +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 @@ -38,7 +45,7 @@ def device_state_attributes(self): return attributes -class HueTemperature(GenericZLLSensor, Entity): +class HueTemperature(GenericHueGaugeSensorEntity): """The temperature sensor entity for a Hue motion sensor device.""" device_class = DEVICE_CLASS_TEMPERATURE diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 8de472e7894872..16476840f1b592 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -7,6 +7,7 @@ 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 @@ -164,7 +165,8 @@ async def async_update_items(self): for item_id in api: existing = current.get(api[item_id].uniqueid) if existing is not None: - self.hass.async_create_task(existing.async_update_ha_state()) + self.hass.async_create_task( + existing.async_maybe_update_ha_state()) continue primary_sensor = None @@ -206,6 +208,9 @@ def __init__(self, sensor, name, bridge, primary_sensor=None): 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.""" @@ -237,6 +242,17 @@ 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. From 9958a6f7b1afc76f344b768d4b10ee1761b8d676 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 17 Apr 2019 14:14:40 -0700 Subject: [PATCH 28/28] relative imports --- homeassistant/components/hue/sensor_base.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 16476840f1b592..1d6fa2d34b4795 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -53,9 +53,8 @@ class SensorManager: def __init__(self, hass, bridge): """Initialize the sensor manager.""" import aiohue - from homeassistant.components.hue.binary_sensor import ( - HuePresence, PRESENCE_NAME_FORMAT) - from homeassistant.components.hue.sensor import ( + from .binary_sensor import HuePresence, PRESENCE_NAME_FORMAT + from .sensor import ( HueLightLevel, HueTemperature, LIGHT_LEVEL_NAME_FORMAT, TEMPERATURE_NAME_FORMAT)