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.pypull/11470/head
parent
889eef78e4
commit
5a469f4d4b
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue