Support multiple Hue bridges with lights of the same id (#11259)

* Improve support for multiple Hue bridges with lights that have the same id.

The old code pre-refactoring kept a per-bridge list of lights in a closure; my refactoring moved that to hass.data, which is convenient but caused them to conflict with each other.

Fixes #11183

* Update test_hue.py
pull/11470/head
Andrea Campi 2017-12-24 00:12:54 +00:00 committed by Paulus Schoutsen
parent 889eef78e4
commit 5a469f4d4b
3 changed files with 113 additions and 63 deletions

View File

@ -160,6 +160,8 @@ class HueBridge(object):
self.allow_hue_groups = allow_hue_groups
self.bridge = None
self.lights = {}
self.lightgroups = {}
self.configured = False
self.config_request_id = None

View File

@ -31,10 +31,6 @@ DEPENDENCIES = ['hue']
_LOGGER = logging.getLogger(__name__)
DATA_KEY = 'hue_lights'
DATA_LIGHTS = 'lights'
DATA_LIGHTGROUPS = 'lightgroups'
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
@ -93,8 +89,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is None or 'bridge_id' not in discovery_info:
return
setup_data(hass)
if config is not None and len(config) > 0:
# Legacy configuration, will be removed in 0.60
config_str = yaml.dump([config])
@ -110,12 +104,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
unthrottled_update_lights(hass, bridge, add_devices)
def setup_data(hass):
"""Initialize internal data. Useful from tests."""
if DATA_KEY not in hass.data:
hass.data[DATA_KEY] = {DATA_LIGHTS: {}, DATA_LIGHTGROUPS: {}}
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_lights(hass, bridge, add_devices):
"""Update the Hue light objects with latest info from the bridge."""
@ -176,18 +164,17 @@ def process_lights(hass, api, bridge, bridge_type, update_lights_cb):
new_lights = []
lights = hass.data[DATA_KEY][DATA_LIGHTS]
for light_id, info in api_lights.items():
if light_id not in lights:
lights[light_id] = HueLight(
if light_id not in bridge.lights:
bridge.lights[light_id] = HueLight(
int(light_id), info, bridge,
update_lights_cb,
bridge_type, bridge.allow_unreachable,
bridge.allow_in_emulated_hue)
new_lights.append(lights[light_id])
new_lights.append(bridge.lights[light_id])
else:
lights[light_id].info = info
lights[light_id].schedule_update_ha_state()
bridge.lights[light_id].info = info
bridge.lights[light_id].schedule_update_ha_state()
return new_lights
@ -202,23 +189,22 @@ def process_groups(hass, api, bridge, bridge_type, update_lights_cb):
new_lights = []
groups = hass.data[DATA_KEY][DATA_LIGHTGROUPS]
for lightgroup_id, info in api_groups.items():
if 'state' not in info:
_LOGGER.warning('Group info does not contain state. '
'Please update your hub.')
return []
if lightgroup_id not in groups:
groups[lightgroup_id] = HueLight(
if lightgroup_id not in bridge.lightgroups:
bridge.lightgroups[lightgroup_id] = HueLight(
int(lightgroup_id), info, bridge,
update_lights_cb,
bridge_type, bridge.allow_unreachable,
bridge.allow_in_emulated_hue, True)
new_lights.append(groups[lightgroup_id])
new_lights.append(bridge.lightgroups[lightgroup_id])
else:
groups[lightgroup_id].info = info
groups[lightgroup_id].schedule_update_ha_state()
bridge.lightgroups[lightgroup_id].info = info
bridge.lightgroups[lightgroup_id].schedule_update_ha_state()
return new_lights

View File

@ -36,27 +36,45 @@ class TestSetup(unittest.TestCase):
self.mock_lights = []
self.mock_groups = []
self.mock_add_devices = MagicMock()
hue_light.setup_data(self.hass)
def setup_mocks_for_process_lights(self):
"""Set up all mocks for process_lights tests."""
self.mock_bridge = MagicMock()
self.mock_bridge = self.create_mock_bridge('host')
self.mock_api = MagicMock()
self.mock_api.get.return_value = {}
self.mock_bridge.get_api.return_value = self.mock_api
self.mock_bridge_type = MagicMock()
hue_light.setup_data(self.hass)
def setup_mocks_for_process_groups(self):
"""Set up all mocks for process_groups tests."""
self.mock_bridge = MagicMock()
self.mock_bridge = self.create_mock_bridge('host')
self.mock_bridge.get_group.return_value = {
'name': 'Group 0', 'state': {'any_on': True}}
self.mock_api = MagicMock()
self.mock_api.get.return_value = {}
self.mock_bridge.get_api.return_value = self.mock_api
self.mock_bridge_type = MagicMock()
hue_light.setup_data(self.hass)
def create_mock_bridge(self, host, allow_hue_groups=True):
"""Return a mock HueBridge with reasonable defaults."""
mock_bridge = MagicMock()
mock_bridge.host = host
mock_bridge.allow_hue_groups = allow_hue_groups
mock_bridge.lights = {}
mock_bridge.lightgroups = {}
return mock_bridge
def create_mock_lights(self, lights):
"""Return a dict suitable for mocking api.get('lights')."""
mock_bridge_lights = lights
for light_id, info in mock_bridge_lights.items():
if 'state' not in info:
info['state'] = {'on': False}
return mock_bridge_lights
def test_setup_platform_no_discovery_info(self):
"""Test setup_platform without discovery info."""
@ -211,6 +229,70 @@ class TestSetup(unittest.TestCase):
self.mock_add_devices.assert_called_once_with(
self.mock_lights)
@MockDependency('phue')
def test_update_lights_with_two_bridges(self, mock_phue):
"""Test the update_lights function with two bridges."""
self.setup_mocks_for_update_lights()
mock_bridge_one = self.create_mock_bridge('one', False)
mock_bridge_one_lights = self.create_mock_lights(
{1: {'name': 'b1l1'}, 2: {'name': 'b1l2'}})
mock_bridge_two = self.create_mock_bridge('two', False)
mock_bridge_two_lights = self.create_mock_lights(
{1: {'name': 'b2l1'}, 3: {'name': 'b2l3'}})
with patch('homeassistant.components.light.hue.get_bridge_type',
return_value=self.mock_bridge_type):
with patch('homeassistant.components.light.hue.HueLight.'
'schedule_update_ha_state'):
mock_api = MagicMock()
mock_api.get.return_value = mock_bridge_one_lights
with patch.object(mock_bridge_one, 'get_api',
return_value=mock_api):
hue_light.unthrottled_update_lights(
self.hass, mock_bridge_one, self.mock_add_devices)
mock_api = MagicMock()
mock_api.get.return_value = mock_bridge_two_lights
with patch.object(mock_bridge_two, 'get_api',
return_value=mock_api):
hue_light.unthrottled_update_lights(
self.hass, mock_bridge_two, self.mock_add_devices)
self.assertEquals(sorted(mock_bridge_one.lights.keys()), [1, 2])
self.assertEquals(sorted(mock_bridge_two.lights.keys()), [1, 3])
self.assertEquals(len(self.mock_add_devices.mock_calls), 2)
# first call
name, args, kwargs = self.mock_add_devices.mock_calls[0]
self.assertEquals(len(args), 1)
self.assertEquals(len(kwargs), 0)
# one argument, a list of lights in bridge one; each of them is an
# object of type HueLight so we can't straight up compare them
lights = args[0]
self.assertEquals(
lights[0].unique_id,
'{}.b1l1.Light.1'.format(hue_light.HueLight))
self.assertEquals(
lights[1].unique_id,
'{}.b1l2.Light.2'.format(hue_light.HueLight))
# second call works the same
name, args, kwargs = self.mock_add_devices.mock_calls[1]
self.assertEquals(len(args), 1)
self.assertEquals(len(kwargs), 0)
lights = args[0]
self.assertEquals(
lights[0].unique_id,
'{}.b2l1.Light.1'.format(hue_light.HueLight))
self.assertEquals(
lights[1].unique_id,
'{}.b2l3.Light.3'.format(hue_light.HueLight))
def test_process_lights_api_error(self):
"""Test the process_lights function when the bridge errors out."""
self.setup_mocks_for_process_lights()
@ -221,9 +303,7 @@ class TestSetup(unittest.TestCase):
None)
self.assertEquals([], ret)
self.assertEquals(
{},
self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTS])
self.assertEquals(self.mock_bridge.lights, {})
def test_process_lights_no_lights(self):
"""Test the process_lights function when bridge returns no lights."""
@ -234,9 +314,7 @@ class TestSetup(unittest.TestCase):
None)
self.assertEquals([], ret)
self.assertEquals(
{},
self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTS])
self.assertEquals(self.mock_bridge.lights, {})
@patch('homeassistant.components.light.hue.HueLight')
def test_process_lights_some_lights(self, mock_hue_light):
@ -260,9 +338,7 @@ class TestSetup(unittest.TestCase):
self.mock_bridge_type, self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue),
])
self.assertEquals(
len(self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTS]),
2)
self.assertEquals(len(self.mock_bridge.lights), 2)
@patch('homeassistant.components.light.hue.HueLight')
def test_process_lights_new_light(self, mock_hue_light):
@ -274,8 +350,7 @@ class TestSetup(unittest.TestCase):
self.setup_mocks_for_process_lights()
self.mock_api.get.return_value = {
1: {'state': 'on'}, 2: {'state': 'off'}}
self.hass.data[
hue_light.DATA_KEY][hue_light.DATA_LIGHTS][1] = MagicMock()
self.mock_bridge.lights = {1: MagicMock()}
ret = hue_light.process_lights(
self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type,
@ -288,11 +363,9 @@ class TestSetup(unittest.TestCase):
self.mock_bridge_type, self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue),
])
self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTS][
1].schedule_update_ha_state.assert_called_once_with()
self.assertEquals(
len(self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTS]),
2)
self.assertEquals(len(self.mock_bridge.lights), 2)
self.mock_bridge.lights[1]\
.schedule_update_ha_state.assert_called_once_with()
def test_process_groups_api_error(self):
"""Test the process_groups function when the bridge errors out."""
@ -304,9 +377,7 @@ class TestSetup(unittest.TestCase):
None)
self.assertEquals([], ret)
self.assertEquals(
{},
self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTGROUPS])
self.assertEquals(self.mock_bridge.lightgroups, {})
def test_process_groups_no_state(self):
"""Test the process_groups function when bridge returns no status."""
@ -318,9 +389,7 @@ class TestSetup(unittest.TestCase):
None)
self.assertEquals([], ret)
self.assertEquals(
{},
self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTGROUPS])
self.assertEquals(self.mock_bridge.lightgroups, {})
@patch('homeassistant.components.light.hue.HueLight')
def test_process_groups_some_groups(self, mock_hue_light):
@ -344,10 +413,7 @@ class TestSetup(unittest.TestCase):
self.mock_bridge_type, self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue, True),
])
self.assertEquals(
len(self.hass.data[
hue_light.DATA_KEY][hue_light.DATA_LIGHTGROUPS]),
2)
self.assertEquals(len(self.mock_bridge.lightgroups), 2)
@patch('homeassistant.components.light.hue.HueLight')
def test_process_groups_new_group(self, mock_hue_light):
@ -359,8 +425,7 @@ class TestSetup(unittest.TestCase):
self.setup_mocks_for_process_groups()
self.mock_api.get.return_value = {
1: {'state': 'on'}, 2: {'state': 'off'}}
self.hass.data[
hue_light.DATA_KEY][hue_light.DATA_LIGHTGROUPS][1] = MagicMock()
self.mock_bridge.lightgroups = {1: MagicMock()}
ret = hue_light.process_groups(
self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type,
@ -373,12 +438,9 @@ class TestSetup(unittest.TestCase):
self.mock_bridge_type, self.mock_bridge.allow_unreachable,
self.mock_bridge.allow_in_emulated_hue, True),
])
self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTGROUPS][
1].schedule_update_ha_state.assert_called_once_with()
self.assertEquals(
len(self.hass.data[
hue_light.DATA_KEY][hue_light.DATA_LIGHTGROUPS]),
2)
self.assertEquals(len(self.mock_bridge.lightgroups), 2)
self.mock_bridge.lightgroups[1]\
.schedule_update_ha_state.assert_called_once_with()
class TestHueLight(unittest.TestCase):