Try to fix crashes after Hue refactoring (#11270)

* Try to fix crashes after Hue refactoring

Refs #11183

* Fix usage of dispatcher_send via helper.

* Address review feedback.
pull/11563/head
Andrea Campi 2018-01-10 07:05:04 +00:00 committed by Martin Hjelmare
parent 3cba09c6f6
commit 4dda842b16
3 changed files with 202 additions and 130 deletions

View File

@ -152,6 +152,7 @@ class HueBridge(object):
allow_in_emulated_hue=True, allow_hue_groups=True):
"""Initialize the system."""
self.host = host
self.bridge_id = socket.gethostbyname(host)
self.hass = hass
self.filename = filename
self.allow_unreachable = allow_unreachable
@ -165,7 +166,7 @@ class HueBridge(object):
self.configured = False
self.config_request_id = None
hass.data[DOMAIN][socket.gethostbyname(host)] = self
hass.data[DOMAIN][self.bridge_id] = self
def setup(self):
"""Set up a phue bridge based on host parameter."""
@ -196,7 +197,7 @@ class HueBridge(object):
discovery.load_platform(
self.hass, 'light', DOMAIN,
{'bridge_id': socket.gethostbyname(self.host)})
{'bridge_id': self.bridge_id})
# create a service for calling run_scene directly on the bridge,
# used to simplify automation rules.

View File

@ -4,6 +4,7 @@ This component provides light support for the Philips Hue system.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.hue/
"""
import asyncio
from datetime import timedelta
import logging
import random
@ -14,9 +15,6 @@ import voluptuous as vol
import homeassistant.components.hue as hue
import homeassistant.util as util
from homeassistant.util import yaml
import homeassistant.util.color as color_util
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR,
ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_COLORLOOP, EFFECT_RANDOM,
@ -24,8 +22,10 @@ from homeassistant.components.light import (
SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION,
SUPPORT_XY_COLOR, Light, PLATFORM_SCHEMA)
from homeassistant.const import CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME
from homeassistant.components.emulated_hue import ATTR_EMULATED_HUE_HIDDEN
import homeassistant.helpers.config_validation as cv
import homeassistant.util as util
from homeassistant.util import yaml
import homeassistant.util.color as color_util
DEPENDENCIES = ['hue']
@ -49,6 +49,7 @@ SUPPORT_HUE = {
'Color temperature light': SUPPORT_HUE_COLOR_TEMP
}
ATTR_EMULATED_HUE_HIDDEN = 'emulated_hue_hidden'
ATTR_IS_HUE_GROUP = 'is_hue_group'
# Legacy configuration, will be removed in 0.60
@ -83,6 +84,8 @@ This configuration is deprecated, please check the
information.
"""
SIGNAL_CALLBACK = 'hue_light_callback_{}_{}'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Hue lights."""
@ -163,7 +166,10 @@ def process_lights(hass, api, bridge, update_lights_cb):
new_lights.append(bridge.lights[light_id])
else:
bridge.lights[light_id].info = info
bridge.lights[light_id].schedule_update_ha_state()
hass.helpers.dispatcher.dispatcher_send(
SIGNAL_CALLBACK.format(
bridge.bridge_id,
bridge.lights[light_id].light_id))
return new_lights
@ -193,7 +199,10 @@ def process_groups(hass, api, bridge, update_lights_cb):
new_lights.append(bridge.lightgroups[lightgroup_id])
else:
bridge.lightgroups[lightgroup_id].info = info
bridge.lightgroups[lightgroup_id].schedule_update_ha_state()
hass.helpers.dispatcher.dispatcher_send(
SIGNAL_CALLBACK.format(
bridge.bridge_id,
bridge.lightgroups[lightgroup_id].light_id))
return new_lights
@ -366,3 +375,11 @@ class HueLight(Light):
if self.is_group:
attributes[ATTR_IS_HUE_GROUP] = self.is_group
return attributes
@asyncio.coroutine
def async_added_to_hass(self):
"""Register update callback."""
dev_id = self.bridge.bridge_id, self.light_id
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_CALLBACK.format(*dev_id),
self.async_schedule_update_ha_state)

View File

@ -12,6 +12,8 @@ from tests.common import get_test_home_assistant, MockDependency
_LOGGER = logging.getLogger(__name__)
HUE_LIGHT_NS = 'homeassistant.components.light.hue.'
class TestSetup(unittest.TestCase):
"""Test the Hue light platform."""
@ -29,11 +31,10 @@ class TestSetup(unittest.TestCase):
def setup_mocks_for_update_lights(self):
"""Set up all mocks for update_lights tests."""
self.mock_bridge = MagicMock()
self.mock_bridge.bridge_id = 'bridge-id'
self.mock_bridge.allow_hue_groups = False
self.mock_api = MagicMock()
self.mock_bridge.get_api.return_value = self.mock_api
self.mock_lights = []
self.mock_groups = []
self.mock_add_devices = MagicMock()
def setup_mocks_for_process_lights(self):
@ -56,6 +57,7 @@ class TestSetup(unittest.TestCase):
def create_mock_bridge(self, host, allow_hue_groups=True):
"""Return a mock HueBridge with reasonable defaults."""
mock_bridge = MagicMock()
mock_bridge.bridge_id = 'bridge-id'
mock_bridge.host = host
mock_bridge.allow_hue_groups = allow_hue_groups
mock_bridge.lights = {}
@ -72,6 +74,14 @@ class TestSetup(unittest.TestCase):
return mock_bridge_lights
def build_mock_light(self, bridge, light_id, name):
"""Return a mock HueLight."""
light = MagicMock()
light.bridge = bridge
light.light_id = light_id
light.name = name
return light
def test_setup_platform_no_discovery_info(self):
"""Test setup_platform without discovery info."""
self.hass.data[hue.DOMAIN] = {}
@ -96,8 +106,8 @@ class TestSetup(unittest.TestCase):
self.hass.data[hue.DOMAIN] = {'10.0.0.1': mock_bridge}
mock_add_devices = MagicMock()
with patch('homeassistant.components.light.hue.' +
'unthrottled_update_lights') as mock_update_lights:
with patch(HUE_LIGHT_NS + 'unthrottled_update_lights') \
as mock_update_lights:
hue_light.setup_platform(
self.hass, {}, mock_add_devices,
{'bridge_id': '10.0.0.1'})
@ -114,8 +124,8 @@ class TestSetup(unittest.TestCase):
}
mock_add_devices = MagicMock()
with patch('homeassistant.components.light.hue.' +
'unthrottled_update_lights') as mock_update_lights:
with patch(HUE_LIGHT_NS + 'unthrottled_update_lights') \
as mock_update_lights:
hue_light.setup_platform(
self.hass, {}, mock_add_devices,
{'bridge_id': '10.0.0.1'})
@ -133,83 +143,105 @@ class TestSetup(unittest.TestCase):
"""Test the update_lights function when no lights are found."""
self.setup_mocks_for_update_lights()
with patch('homeassistant.components.light.hue.process_lights',
return_value=[]) as mock_process_lights:
with patch('homeassistant.components.light.hue.process_groups',
return_value=self.mock_groups) \
with patch(HUE_LIGHT_NS + 'process_lights', return_value=[]) \
as mock_process_lights:
with patch(HUE_LIGHT_NS + 'process_groups', return_value=[]) \
as mock_process_groups:
hue_light.unthrottled_update_lights(
self.hass, self.mock_bridge, self.mock_add_devices)
with patch.object(self.hass.helpers.dispatcher,
'dispatcher_send') as dispatcher_send:
hue_light.unthrottled_update_lights(
self.hass, self.mock_bridge, self.mock_add_devices)
mock_process_lights.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge, mock.ANY)
mock_process_groups.assert_not_called()
self.mock_add_devices.assert_not_called()
mock_process_lights.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge, mock.ANY)
mock_process_groups.assert_not_called()
self.mock_add_devices.assert_not_called()
dispatcher_send.assert_not_called()
@MockDependency('phue')
def test_update_lights_with_some_lights(self, mock_phue):
"""Test the update_lights function with some lights."""
self.setup_mocks_for_update_lights()
self.mock_lights = ['some', 'light']
mock_lights = [
self.build_mock_light(self.mock_bridge, 42, 'some'),
self.build_mock_light(self.mock_bridge, 84, 'light'),
]
with patch('homeassistant.components.light.hue.process_lights',
return_value=self.mock_lights) as mock_process_lights:
with patch('homeassistant.components.light.hue.process_groups',
return_value=self.mock_groups) \
with patch(HUE_LIGHT_NS + 'process_lights',
return_value=mock_lights) as mock_process_lights:
with patch(HUE_LIGHT_NS + 'process_groups', return_value=[]) \
as mock_process_groups:
hue_light.unthrottled_update_lights(
self.hass, self.mock_bridge, self.mock_add_devices)
with patch.object(self.hass.helpers.dispatcher,
'dispatcher_send') as dispatcher_send:
hue_light.unthrottled_update_lights(
self.hass, self.mock_bridge, self.mock_add_devices)
mock_process_lights.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge, mock.ANY)
mock_process_groups.assert_not_called()
self.mock_add_devices.assert_called_once_with(
self.mock_lights)
mock_process_lights.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge, mock.ANY)
mock_process_groups.assert_not_called()
self.mock_add_devices.assert_called_once_with(
mock_lights)
dispatcher_send.assert_not_called()
@MockDependency('phue')
def test_update_lights_no_groups(self, mock_phue):
"""Test the update_lights function when no groups are found."""
self.setup_mocks_for_update_lights()
self.mock_bridge.allow_hue_groups = True
self.mock_lights = ['some', 'light']
mock_lights = [
self.build_mock_light(self.mock_bridge, 42, 'some'),
self.build_mock_light(self.mock_bridge, 84, 'light'),
]
with patch('homeassistant.components.light.hue.process_lights',
return_value=self.mock_lights) as mock_process_lights:
with patch('homeassistant.components.light.hue.process_groups',
return_value=self.mock_groups) \
with patch(HUE_LIGHT_NS + 'process_lights',
return_value=mock_lights) as mock_process_lights:
with patch(HUE_LIGHT_NS + 'process_groups', return_value=[]) \
as mock_process_groups:
hue_light.unthrottled_update_lights(
self.hass, self.mock_bridge, self.mock_add_devices)
with patch.object(self.hass.helpers.dispatcher,
'dispatcher_send') as dispatcher_send:
hue_light.unthrottled_update_lights(
self.hass, self.mock_bridge, self.mock_add_devices)
mock_process_lights.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge, mock.ANY)
mock_process_groups.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge, mock.ANY)
self.mock_add_devices.assert_called_once_with(
self.mock_lights)
mock_process_lights.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge, mock.ANY)
mock_process_groups.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge, mock.ANY)
self.mock_add_devices.assert_called_once_with(
mock_lights)
dispatcher_send.assert_not_called()
@MockDependency('phue')
def test_update_lights_with_lights_and_groups(self, mock_phue):
"""Test the update_lights function with both lights and groups."""
self.setup_mocks_for_update_lights()
self.mock_bridge.allow_hue_groups = True
self.mock_lights = ['some', 'light']
self.mock_groups = ['and', 'groups']
mock_lights = [
self.build_mock_light(self.mock_bridge, 42, 'some'),
self.build_mock_light(self.mock_bridge, 84, 'light'),
]
mock_groups = [
self.build_mock_light(self.mock_bridge, 15, 'and'),
self.build_mock_light(self.mock_bridge, 72, 'groups'),
]
with patch('homeassistant.components.light.hue.process_lights',
return_value=self.mock_lights) as mock_process_lights:
with patch('homeassistant.components.light.hue.process_groups',
return_value=self.mock_groups) \
as mock_process_groups:
hue_light.unthrottled_update_lights(
self.hass, self.mock_bridge, self.mock_add_devices)
with patch(HUE_LIGHT_NS + 'process_lights',
return_value=mock_lights) as mock_process_lights:
with patch(HUE_LIGHT_NS + 'process_groups',
return_value=mock_groups) as mock_process_groups:
with patch.object(self.hass.helpers.dispatcher,
'dispatcher_send') as dispatcher_send:
hue_light.unthrottled_update_lights(
self.hass, self.mock_bridge, self.mock_add_devices)
mock_process_lights.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge, mock.ANY)
mock_process_groups.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge, mock.ANY)
self.mock_add_devices.assert_called_once_with(
self.mock_lights)
mock_process_lights.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge, mock.ANY)
mock_process_groups.assert_called_once_with(
self.hass, self.mock_api, self.mock_bridge, mock.ANY)
# note that mock_lights has been modified in place and
# now contains both lights and groups
self.mock_add_devices.assert_called_once_with(
mock_lights)
dispatcher_send.assert_not_called()
@MockDependency('phue')
def test_update_lights_with_two_bridges(self, mock_phue):
@ -288,36 +320,42 @@ class TestSetup(unittest.TestCase):
"""Test the process_lights function when bridge returns no lights."""
self.setup_mocks_for_process_lights()
ret = hue_light.process_lights(
self.hass, self.mock_api, self.mock_bridge, None)
with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \
as mock_dispatcher_send:
ret = hue_light.process_lights(
self.hass, self.mock_api, self.mock_bridge, None)
self.assertEquals([], ret)
self.assertEquals(self.mock_bridge.lights, {})
self.assertEquals([], ret)
mock_dispatcher_send.assert_not_called()
self.assertEquals(self.mock_bridge.lights, {})
@patch('homeassistant.components.light.hue.HueLight')
@patch(HUE_LIGHT_NS + 'HueLight')
def test_process_lights_some_lights(self, mock_hue_light):
"""Test the process_lights function with multiple groups."""
self.setup_mocks_for_process_lights()
self.mock_api.get.return_value = {
1: {'state': 'on'}, 2: {'state': 'off'}}
ret = hue_light.process_lights(
self.hass, self.mock_api, self.mock_bridge, None)
with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \
as mock_dispatcher_send:
ret = hue_light.process_lights(
self.hass, self.mock_api, self.mock_bridge, None)
self.assertEquals(len(ret), 2)
mock_hue_light.assert_has_calls([
call(
1, {'state': 'on'}, self.mock_bridge, mock.ANY,
self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue),
call(
2, {'state': 'off'}, self.mock_bridge, mock.ANY,
self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue),
])
self.assertEquals(len(self.mock_bridge.lights), 2)
self.assertEquals(len(ret), 2)
mock_hue_light.assert_has_calls([
call(
1, {'state': 'on'}, self.mock_bridge, mock.ANY,
self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue),
call(
2, {'state': 'off'}, self.mock_bridge, mock.ANY,
self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue),
])
mock_dispatcher_send.assert_not_called()
self.assertEquals(len(self.mock_bridge.lights), 2)
@patch('homeassistant.components.light.hue.HueLight')
@patch(HUE_LIGHT_NS + 'HueLight')
def test_process_lights_new_light(self, mock_hue_light):
"""
Test the process_lights function with new groups.
@ -327,21 +365,24 @@ class TestSetup(unittest.TestCase):
self.setup_mocks_for_process_lights()
self.mock_api.get.return_value = {
1: {'state': 'on'}, 2: {'state': 'off'}}
self.mock_bridge.lights = {1: MagicMock()}
self.mock_bridge.lights = {
1: self.build_mock_light(self.mock_bridge, 1, 'foo')}
ret = hue_light.process_lights(
self.hass, self.mock_api, self.mock_bridge, None)
with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \
as mock_dispatcher_send:
ret = hue_light.process_lights(
self.hass, self.mock_api, self.mock_bridge, None)
self.assertEquals(len(ret), 1)
mock_hue_light.assert_has_calls([
call(
2, {'state': 'off'}, self.mock_bridge, mock.ANY,
self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue),
])
self.assertEquals(len(self.mock_bridge.lights), 2)
self.mock_bridge.lights[1]\
.schedule_update_ha_state.assert_called_once_with()
self.assertEquals(len(ret), 1)
mock_hue_light.assert_has_calls([
call(
2, {'state': 'off'}, self.mock_bridge, mock.ANY,
self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue),
])
mock_dispatcher_send.assert_called_once_with(
'hue_light_callback_bridge-id_1')
self.assertEquals(len(self.mock_bridge.lights), 2)
def test_process_groups_api_error(self):
"""Test the process_groups function when the bridge errors out."""
@ -359,36 +400,42 @@ class TestSetup(unittest.TestCase):
self.setup_mocks_for_process_groups()
self.mock_bridge.get_group.return_value = {'name': 'Group 0'}
ret = hue_light.process_groups(
self.hass, self.mock_api, self.mock_bridge, None)
with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \
as mock_dispatcher_send:
ret = hue_light.process_groups(
self.hass, self.mock_api, self.mock_bridge, None)
self.assertEquals([], ret)
self.assertEquals(self.mock_bridge.lightgroups, {})
self.assertEquals([], ret)
mock_dispatcher_send.assert_not_called()
self.assertEquals(self.mock_bridge.lightgroups, {})
@patch('homeassistant.components.light.hue.HueLight')
@patch(HUE_LIGHT_NS + 'HueLight')
def test_process_groups_some_groups(self, mock_hue_light):
"""Test the process_groups function with multiple groups."""
self.setup_mocks_for_process_groups()
self.mock_api.get.return_value = {
1: {'state': 'on'}, 2: {'state': 'off'}}
ret = hue_light.process_groups(
self.hass, self.mock_api, self.mock_bridge, None)
with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \
as mock_dispatcher_send:
ret = hue_light.process_groups(
self.hass, self.mock_api, self.mock_bridge, None)
self.assertEquals(len(ret), 2)
mock_hue_light.assert_has_calls([
call(
1, {'state': 'on'}, self.mock_bridge, mock.ANY,
self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue, True),
call(
2, {'state': 'off'}, self.mock_bridge, mock.ANY,
self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue, True),
])
self.assertEquals(len(self.mock_bridge.lightgroups), 2)
self.assertEquals(len(ret), 2)
mock_hue_light.assert_has_calls([
call(
1, {'state': 'on'}, self.mock_bridge, mock.ANY,
self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue, True),
call(
2, {'state': 'off'}, self.mock_bridge, mock.ANY,
self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue, True),
])
mock_dispatcher_send.assert_not_called()
self.assertEquals(len(self.mock_bridge.lightgroups), 2)
@patch('homeassistant.components.light.hue.HueLight')
@patch(HUE_LIGHT_NS + 'HueLight')
def test_process_groups_new_group(self, mock_hue_light):
"""
Test the process_groups function with new groups.
@ -398,21 +445,24 @@ class TestSetup(unittest.TestCase):
self.setup_mocks_for_process_groups()
self.mock_api.get.return_value = {
1: {'state': 'on'}, 2: {'state': 'off'}}
self.mock_bridge.lightgroups = {1: MagicMock()}
self.mock_bridge.lightgroups = {
1: self.build_mock_light(self.mock_bridge, 1, 'foo')}
ret = hue_light.process_groups(
self.hass, self.mock_api, self.mock_bridge, None)
with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \
as mock_dispatcher_send:
ret = hue_light.process_groups(
self.hass, self.mock_api, self.mock_bridge, None)
self.assertEquals(len(ret), 1)
mock_hue_light.assert_has_calls([
call(
2, {'state': 'off'}, self.mock_bridge, mock.ANY,
self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue, True),
])
self.assertEquals(len(self.mock_bridge.lightgroups), 2)
self.mock_bridge.lightgroups[1]\
.schedule_update_ha_state.assert_called_once_with()
self.assertEquals(len(ret), 1)
mock_hue_light.assert_has_calls([
call(
2, {'state': 'off'}, self.mock_bridge, mock.ANY,
self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue, True),
])
mock_dispatcher_send.assert_called_once_with(
'hue_light_callback_bridge-id_1')
self.assertEquals(len(self.mock_bridge.lightgroups), 2)
class TestHueLight(unittest.TestCase):
@ -440,6 +490,10 @@ class TestHueLight(unittest.TestCase):
def buildLight(
self, light_id=None, info=None, update_lights=None, is_group=None):
"""Helper to build a HueLight object with minimal fuss."""
if 'state' not in info:
on_key = 'any_on' if is_group is not None else 'on'
info['state'] = {on_key: False}
return hue_light.HueLight(
light_id if light_id is not None else self.light_id,
info if info is not None else self.mock_info,