"""Philips Hue lights platform tests.""" import asyncio from collections import deque import logging from unittest.mock import Mock import aiohue from aiohue.groups import Groups from aiohue.lights import Lights import pytest from homeassistant import config_entries from homeassistant.components import hue from homeassistant.components.hue import light as hue_light from homeassistant.util import color _LOGGER = logging.getLogger(__name__) HUE_LIGHT_NS = "homeassistant.components.light.hue." GROUP_RESPONSE = { "1": { "name": "Group 1", "lights": ["1", "2"], "type": "LightGroup", "action": { "on": True, "bri": 254, "hue": 10000, "sat": 254, "effect": "none", "xy": [0.5, 0.5], "ct": 250, "alert": "select", "colormode": "ct", }, "state": {"any_on": True, "all_on": False}, }, "2": { "name": "Group 2", "lights": ["3", "4", "5"], "type": "LightGroup", "action": { "on": True, "bri": 153, "hue": 4345, "sat": 254, "effect": "none", "xy": [0.5, 0.5], "ct": 250, "alert": "select", "colormode": "ct", }, "state": {"any_on": True, "all_on": False}, }, } LIGHT_1_ON = { "state": { "on": True, "bri": 144, "hue": 13088, "sat": 212, "xy": [0.5128, 0.4147], "ct": 467, "alert": "none", "effect": "none", "colormode": "xy", "reachable": True, }, "capabilities": { "control": { "colorgamuttype": "A", "colorgamut": [[0.704, 0.296], [0.2151, 0.7106], [0.138, 0.08]], } }, "type": "Extended color light", "name": "Hue Lamp 1", "modelid": "LCT001", "swversion": "66009461", "manufacturername": "Philips", "uniqueid": "456", } LIGHT_1_OFF = { "state": { "on": False, "bri": 0, "hue": 0, "sat": 0, "xy": [0, 0], "ct": 0, "alert": "none", "effect": "none", "colormode": "xy", "reachable": True, }, "capabilities": { "control": { "colorgamuttype": "A", "colorgamut": [[0.704, 0.296], [0.2151, 0.7106], [0.138, 0.08]], } }, "type": "Extended color light", "name": "Hue Lamp 1", "modelid": "LCT001", "swversion": "66009461", "manufacturername": "Philips", "uniqueid": "456", } LIGHT_2_OFF = { "state": { "on": False, "bri": 0, "hue": 0, "sat": 0, "xy": [0, 0], "ct": 0, "alert": "none", "effect": "none", "colormode": "hs", "reachable": True, }, "capabilities": { "control": { "colorgamuttype": "A", "colorgamut": [[0.704, 0.296], [0.2151, 0.7106], [0.138, 0.08]], } }, "type": "Extended color light", "name": "Hue Lamp 2", "modelid": "LCT001", "swversion": "66009461", "manufacturername": "Philips", "uniqueid": "123", } LIGHT_2_ON = { "state": { "on": True, "bri": 100, "hue": 13088, "sat": 210, "xy": [0.5, 0.4], "ct": 420, "alert": "none", "effect": "none", "colormode": "hs", "reachable": True, }, "capabilities": { "control": { "colorgamuttype": "A", "colorgamut": [[0.704, 0.296], [0.2151, 0.7106], [0.138, 0.08]], } }, "type": "Extended color light", "name": "Hue Lamp 2 new", "modelid": "LCT001", "swversion": "66009461", "manufacturername": "Philips", "uniqueid": "123", } LIGHT_RESPONSE = {"1": LIGHT_1_ON, "2": LIGHT_2_OFF} LIGHT_RAW = { "capabilities": { "control": { "colorgamuttype": "A", "colorgamut": [[0.704, 0.296], [0.2151, 0.7106], [0.138, 0.08]], } }, "swversion": "66009461", } LIGHT_GAMUT = color.GamutType( color.XYPoint(0.704, 0.296), color.XYPoint(0.2151, 0.7106), color.XYPoint(0.138, 0.08), ) LIGHT_GAMUT_TYPE = "A" @pytest.fixture def mock_bridge(hass): """Mock a Hue bridge.""" bridge = Mock( hass=hass, available=True, authorized=True, allow_unreachable=False, allow_groups=False, api=Mock(), reset_jobs=[], 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_light_responses = deque() bridge.mock_group_responses = deque() async def mock_request(method, path, **kwargs): kwargs["method"] = method kwargs["path"] = path bridge.mock_requests.append(kwargs) if path == "lights": return bridge.mock_light_responses.popleft() if path == "groups": return bridge.mock_group_responses.popleft() return None async def async_request_call(task): await task() bridge.async_request_call = async_request_call bridge.api.config.apiversion = "9.9.9" bridge.api.lights = Lights({}, mock_request) bridge.api.groups = Groups({}, mock_request) return bridge async def setup_bridge(hass, mock_bridge): """Load the Hue light platform with the provided bridge.""" hass.config.components.add(hue.DOMAIN) config_entry = config_entries.ConfigEntry( 1, hue.DOMAIN, "Mock Title", {"host": "mock-host"}, "test", config_entries.CONN_CLASS_LOCAL_POLL, system_options={}, ) mock_bridge.config_entry = config_entry hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge} await hass.config_entries.async_forward_entry_setup(config_entry, "light") # To flush out the service call to update the group await hass.async_block_till_done() async def test_not_load_groups_if_old_bridge(hass, mock_bridge): """Test that we don't try to load gorups if bridge runs old software.""" mock_bridge.api.config.apiversion = "1.12.0" mock_bridge.mock_light_responses.append({}) mock_bridge.mock_group_responses.append(GROUP_RESPONSE) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 1 assert len(hass.states.async_all()) == 0 async def test_no_lights_or_groups(hass, mock_bridge): """Test the update_lights function when no lights are found.""" mock_bridge.allow_groups = True mock_bridge.mock_light_responses.append({}) mock_bridge.mock_group_responses.append({}) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 2 assert len(hass.states.async_all()) == 0 async def test_lights(hass, mock_bridge): """Test the update_lights function with some lights.""" mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 1 # 2 lights assert len(hass.states.async_all()) == 2 lamp_1 = hass.states.get("light.hue_lamp_1") assert lamp_1 is not None assert lamp_1.state == "on" assert lamp_1.attributes["brightness"] == 144 assert lamp_1.attributes["hs_color"] == (36.067, 69.804) lamp_2 = hass.states.get("light.hue_lamp_2") assert lamp_2 is not None assert lamp_2.state == "off" async def test_lights_color_mode(hass, mock_bridge): """Test that lights only report appropriate color mode.""" mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) await setup_bridge(hass, mock_bridge) lamp_1 = hass.states.get("light.hue_lamp_1") assert lamp_1 is not None assert lamp_1.state == "on" assert lamp_1.attributes["brightness"] == 144 assert lamp_1.attributes["hs_color"] == (36.067, 69.804) assert "color_temp" not in lamp_1.attributes new_light1_on = LIGHT_1_ON.copy() new_light1_on["state"] = new_light1_on["state"].copy() new_light1_on["state"]["colormode"] = "ct" mock_bridge.mock_light_responses.append({"1": new_light1_on}) mock_bridge.mock_group_responses.append({}) # Calling a service will trigger the updates to run await hass.services.async_call( "light", "turn_on", {"entity_id": "light.hue_lamp_2"}, blocking=True ) # 2x light update, 1 turn on request assert len(mock_bridge.mock_requests) == 3 lamp_1 = hass.states.get("light.hue_lamp_1") assert lamp_1 is not None assert lamp_1.state == "on" assert lamp_1.attributes["brightness"] == 144 assert lamp_1.attributes["color_temp"] == 467 assert "hs_color" not in lamp_1.attributes async def test_groups(hass, mock_bridge): """Test the update_lights function with some lights.""" mock_bridge.allow_groups = True mock_bridge.mock_light_responses.append({}) mock_bridge.mock_group_responses.append(GROUP_RESPONSE) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 2 # 2 hue group lights assert len(hass.states.async_all()) == 2 lamp_1 = hass.states.get("light.group_1") assert lamp_1 is not None assert lamp_1.state == "on" assert lamp_1.attributes["brightness"] == 254 assert lamp_1.attributes["color_temp"] == 250 lamp_2 = hass.states.get("light.group_2") assert lamp_2 is not None assert lamp_2.state == "on" async def test_new_group_discovered(hass, mock_bridge): """Test if 2nd update has a new group.""" mock_bridge.allow_groups = True mock_bridge.mock_light_responses.append({}) mock_bridge.mock_group_responses.append(GROUP_RESPONSE) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 2 assert len(hass.states.async_all()) == 2 new_group_response = dict(GROUP_RESPONSE) new_group_response["3"] = { "name": "Group 3", "lights": ["3", "4", "5"], "type": "LightGroup", "action": { "on": True, "bri": 153, "hue": 4345, "sat": 254, "effect": "none", "xy": [0.5, 0.5], "ct": 250, "alert": "select", "colormode": "ct", }, "state": {"any_on": True, "all_on": False}, } mock_bridge.mock_light_responses.append({}) mock_bridge.mock_group_responses.append(new_group_response) # Calling a service will trigger the updates to run await hass.services.async_call( "light", "turn_on", {"entity_id": "light.group_1"}, blocking=True ) # 2x group update, 1x light update, 1 turn on request assert len(mock_bridge.mock_requests) == 4 assert len(hass.states.async_all()) == 3 new_group = hass.states.get("light.group_3") assert new_group is not None assert new_group.state == "on" assert new_group.attributes["brightness"] == 153 assert new_group.attributes["color_temp"] == 250 async def test_new_light_discovered(hass, mock_bridge): """Test if 2nd update has a new light.""" mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 1 assert len(hass.states.async_all()) == 2 new_light_response = dict(LIGHT_RESPONSE) new_light_response["3"] = { "state": { "on": False, "bri": 0, "hue": 0, "sat": 0, "xy": [0, 0], "ct": 0, "alert": "none", "effect": "none", "colormode": "hs", "reachable": True, }, "capabilities": { "control": { "colorgamuttype": "A", "colorgamut": [[0.704, 0.296], [0.2151, 0.7106], [0.138, 0.08]], } }, "type": "Extended color light", "name": "Hue Lamp 3", "modelid": "LCT001", "swversion": "66009461", "manufacturername": "Philips", "uniqueid": "789", } mock_bridge.mock_light_responses.append(new_light_response) # Calling a service will trigger the updates to run await hass.services.async_call( "light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True ) # 2x light update, 1 turn on request assert len(mock_bridge.mock_requests) == 3 assert len(hass.states.async_all()) == 3 light = hass.states.get("light.hue_lamp_3") assert light is not None assert light.state == "off" async def test_group_removed(hass, mock_bridge): """Test if 2nd update has removed group.""" mock_bridge.allow_groups = True mock_bridge.mock_light_responses.append({}) mock_bridge.mock_group_responses.append(GROUP_RESPONSE) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 2 assert len(hass.states.async_all()) == 2 mock_bridge.mock_light_responses.append({}) mock_bridge.mock_group_responses.append({"1": GROUP_RESPONSE["1"]}) # Calling a service will trigger the updates to run await hass.services.async_call( "light", "turn_on", {"entity_id": "light.group_1"}, blocking=True ) # 2x group update, 1x light update, 1 turn on request assert len(mock_bridge.mock_requests) == 4 assert len(hass.states.async_all()) == 1 group = hass.states.get("light.group_1") assert group is not None removed_group = hass.states.get("light.group_2") assert removed_group is None async def test_light_removed(hass, mock_bridge): """Test if 2nd update has removed light.""" mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 1 assert len(hass.states.async_all()) == 2 mock_bridge.mock_light_responses.clear() mock_bridge.mock_light_responses.append({"1": LIGHT_RESPONSE.get("1")}) # Calling a service will trigger the updates to run await hass.services.async_call( "light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True ) # 2x light update, 1 turn on request assert len(mock_bridge.mock_requests) == 3 assert len(hass.states.async_all()) == 1 light = hass.states.get("light.hue_lamp_1") assert light is not None removed_light = hass.states.get("light.hue_lamp_2") assert removed_light is None async def test_other_group_update(hass, mock_bridge): """Test changing one group that will impact the state of other light.""" mock_bridge.allow_groups = True mock_bridge.mock_light_responses.append({}) mock_bridge.mock_group_responses.append(GROUP_RESPONSE) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 2 assert len(hass.states.async_all()) == 2 group_2 = hass.states.get("light.group_2") assert group_2 is not None assert group_2.name == "Group 2" assert group_2.state == "on" assert group_2.attributes["brightness"] == 153 assert group_2.attributes["color_temp"] == 250 updated_group_response = dict(GROUP_RESPONSE) updated_group_response["2"] = { "name": "Group 2 new", "lights": ["3", "4", "5"], "type": "LightGroup", "action": { "on": False, "bri": 0, "hue": 0, "sat": 0, "effect": "none", "xy": [0, 0], "ct": 0, "alert": "none", "colormode": "ct", }, "state": {"any_on": False, "all_on": False}, } mock_bridge.mock_light_responses.append({}) mock_bridge.mock_group_responses.append(updated_group_response) # Calling a service will trigger the updates to run await hass.services.async_call( "light", "turn_on", {"entity_id": "light.group_1"}, blocking=True ) # 2x group update, 1x light update, 1 turn on request assert len(mock_bridge.mock_requests) == 4 assert len(hass.states.async_all()) == 2 group_2 = hass.states.get("light.group_2") assert group_2 is not None assert group_2.name == "Group 2 new" assert group_2.state == "off" async def test_other_light_update(hass, mock_bridge): """Test changing one light that will impact state of other light.""" mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 1 assert len(hass.states.async_all()) == 2 lamp_2 = hass.states.get("light.hue_lamp_2") assert lamp_2 is not None assert lamp_2.name == "Hue Lamp 2" assert lamp_2.state == "off" updated_light_response = dict(LIGHT_RESPONSE) updated_light_response["2"] = { "state": { "on": True, "bri": 100, "hue": 13088, "sat": 210, "xy": [0.5, 0.4], "ct": 420, "alert": "none", "effect": "none", "colormode": "hs", "reachable": True, }, "capabilities": { "control": { "colorgamuttype": "A", "colorgamut": [[0.704, 0.296], [0.2151, 0.7106], [0.138, 0.08]], } }, "type": "Extended color light", "name": "Hue Lamp 2 new", "modelid": "LCT001", "swversion": "66009461", "manufacturername": "Philips", "uniqueid": "123", } mock_bridge.mock_light_responses.append(updated_light_response) # Calling a service will trigger the updates to run await hass.services.async_call( "light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True ) # 2x light update, 1 turn on request assert len(mock_bridge.mock_requests) == 3 assert len(hass.states.async_all()) == 2 lamp_2 = hass.states.get("light.hue_lamp_2") assert lamp_2 is not None assert lamp_2.name == "Hue Lamp 2 new" assert lamp_2.state == "on" assert lamp_2.attributes["brightness"] == 100 async def test_update_timeout(hass, mock_bridge): """Test bridge marked as not available if timeout error during update.""" mock_bridge.api.lights.update = Mock(side_effect=asyncio.TimeoutError) mock_bridge.api.groups.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 async def test_update_unauthorized(hass, mock_bridge): """Test bridge marked as not authorized if unauthorized during update.""" mock_bridge.api.lights.update = Mock(side_effect=aiohue.Unauthorized) mock_bridge.api.groups.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 len(mock_bridge.handle_unauthorized_error.mock_calls) == 1 async def test_light_turn_on_service(hass, mock_bridge): """Test calling the turn on service on a light.""" mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) await setup_bridge(hass, mock_bridge) light = hass.states.get("light.hue_lamp_2") assert light is not None assert light.state == "off" updated_light_response = dict(LIGHT_RESPONSE) updated_light_response["2"] = LIGHT_2_ON mock_bridge.mock_light_responses.append(updated_light_response) await hass.services.async_call( "light", "turn_on", {"entity_id": "light.hue_lamp_2", "brightness": 100, "color_temp": 300}, blocking=True, ) # 2x light update, 1 turn on request assert len(mock_bridge.mock_requests) == 3 assert mock_bridge.mock_requests[1]["json"] == { "bri": 100, "on": True, "ct": 300, "alert": "none", } assert len(hass.states.async_all()) == 2 light = hass.states.get("light.hue_lamp_2") assert light is not None assert light.state == "on" # test hue gamut in turn_on service await hass.services.async_call( "light", "turn_on", {"entity_id": "light.hue_lamp_2", "rgb_color": [0, 0, 255]}, blocking=True, ) assert len(mock_bridge.mock_requests) == 5 assert mock_bridge.mock_requests[3]["json"] == { "on": True, "xy": (0.138, 0.08), "alert": "none", } async def test_light_turn_off_service(hass, mock_bridge): """Test calling the turn on service on a light.""" mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) await setup_bridge(hass, mock_bridge) light = hass.states.get("light.hue_lamp_1") assert light is not None assert light.state == "on" updated_light_response = dict(LIGHT_RESPONSE) updated_light_response["1"] = LIGHT_1_OFF mock_bridge.mock_light_responses.append(updated_light_response) await hass.services.async_call( "light", "turn_off", {"entity_id": "light.hue_lamp_1"}, blocking=True ) # 2x light update, 1 turn on request assert len(mock_bridge.mock_requests) == 3 assert mock_bridge.mock_requests[1]["json"] == {"on": False, "alert": "none"} assert len(hass.states.async_all()) == 2 light = hass.states.get("light.hue_lamp_1") assert light is not None assert light.state == "off" def test_available(): """Test available property.""" light = hue_light.HueLight( light=Mock( state={"reachable": False}, raw=LIGHT_RAW, colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), coordinator=Mock(last_update_success=True), bridge=Mock(allow_unreachable=False), is_group=False, ) assert light.available is False light = hue_light.HueLight( light=Mock( state={"reachable": False}, raw=LIGHT_RAW, colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), coordinator=Mock(last_update_success=True), bridge=Mock(allow_unreachable=True), is_group=False, ) assert light.available is True light = hue_light.HueLight( light=Mock( state={"reachable": False}, raw=LIGHT_RAW, colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), coordinator=Mock(last_update_success=True), bridge=Mock(allow_unreachable=False), is_group=True, ) assert light.available is True def test_hs_color(): """Test hs_color property.""" light = hue_light.HueLight( light=Mock( state={"colormode": "ct", "hue": 1234, "sat": 123}, raw=LIGHT_RAW, colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), coordinator=Mock(last_update_success=True), bridge=Mock(), is_group=False, ) assert light.hs_color is None light = hue_light.HueLight( light=Mock( state={"colormode": "hs", "hue": 1234, "sat": 123}, raw=LIGHT_RAW, colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), coordinator=Mock(last_update_success=True), bridge=Mock(), is_group=False, ) assert light.hs_color is None light = hue_light.HueLight( light=Mock( state={"colormode": "xy", "hue": 1234, "sat": 123, "xy": [0.4, 0.5]}, raw=LIGHT_RAW, colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), coordinator=Mock(last_update_success=True), bridge=Mock(), is_group=False, ) assert light.hs_color == color.color_xy_to_hs(0.4, 0.5, LIGHT_GAMUT)