diff --git a/homeassistant/components/hue.py b/homeassistant/components/hue.py index 302c8be7598..a83b55e84e5 100644 --- a/homeassistant/components/hue.py +++ b/homeassistant/components/hue.py @@ -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. diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 64e5dff0d26..f4ea04240f1 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -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) diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index b612fa15931..611f1240d45 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -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,