"""Tradfri lights platform tests.""" from copy import deepcopy from unittest.mock import Mock, MagicMock, patch, PropertyMock import pytest from pytradfri.device import Device, LightControl, Light from homeassistant.components import tradfri from tests.common import MockConfigEntry DEFAULT_TEST_FEATURES = { "can_set_dimmer": False, "can_set_color": False, "can_set_temp": False, } # [ # {bulb features}, # {turn_on arguments}, # {expected result} # ] TURN_ON_TEST_CASES = [ # Turn On [{}, {}, {"state": "on"}], # Brightness > 0 [{"can_set_dimmer": True}, {"brightness": 100}, {"state": "on", "brightness": 100}], # Brightness == 1 [{"can_set_dimmer": True}, {"brightness": 1}, {"brightness": 1}], # Brightness > 254 [{"can_set_dimmer": True}, {"brightness": 1000}, {"brightness": 254}], # color_temp [{"can_set_temp": True}, {"color_temp": 250}, {"color_temp": 250}], # color_temp < 250 [{"can_set_temp": True}, {"color_temp": 1}, {"color_temp": 250}], # color_temp > 454 [{"can_set_temp": True}, {"color_temp": 1000}, {"color_temp": 454}], # hs color [ {"can_set_color": True}, {"hs_color": [300, 100]}, {"state": "on", "hs_color": [300, 100]}, ], # ct + brightness [ {"can_set_dimmer": True, "can_set_temp": True}, {"color_temp": 250, "brightness": 200}, {"state": "on", "color_temp": 250, "brightness": 200}, ], # ct + brightness (no temp support) [ {"can_set_dimmer": True, "can_set_temp": False, "can_set_color": True}, {"color_temp": 250, "brightness": 200}, {"state": "on", "hs_color": [26.807, 34.869], "brightness": 200}, ], # ct + brightness (no temp or color support) [ {"can_set_dimmer": True, "can_set_temp": False, "can_set_color": False}, {"color_temp": 250, "brightness": 200}, {"state": "on", "brightness": 200}, ], # hs + brightness [ {"can_set_dimmer": True, "can_set_color": True}, {"hs_color": [300, 100], "brightness": 200}, {"state": "on", "hs_color": [300, 100], "brightness": 200}, ], ] # Result of transition is not tested, but data is passed to turn on service. TRANSITION_CASES_FOR_TESTS = [None, 0, 1] @pytest.fixture(autouse=True, scope="module") def setup(request): """Set up patches for pytradfri methods.""" p_1 = patch( "pytradfri.device.LightControl.raw", new_callable=PropertyMock, return_value=[{"mock": "mock"}], ) p_2 = patch("pytradfri.device.LightControl.lights") p_1.start() p_2.start() def teardown(): """Remove patches for pytradfri methods.""" p_1.stop() p_2.stop() request.addfinalizer(teardown) @pytest.fixture def mock_gateway(): """Mock a Tradfri gateway.""" def get_devices(): """Return mock devices.""" return gateway.mock_devices def get_groups(): """Return mock groups.""" return gateway.mock_groups gateway = Mock( get_devices=get_devices, get_groups=get_groups, mock_devices=[], mock_groups=[], mock_responses=[], ) return gateway @pytest.fixture def mock_api(mock_gateway): """Mock api.""" async def api(command): """Mock api function.""" # Store the data for "real" command objects. if hasattr(command, "_data") and not isinstance(command, Mock): mock_gateway.mock_responses.append(command._data) return command return api async def generate_psk(self, code): """Mock psk.""" return "mock" async def setup_gateway(hass, mock_gateway, mock_api): """Load the Tradfri platform with a mock gateway.""" entry = MockConfigEntry( domain=tradfri.DOMAIN, data={ "host": "mock-host", "identity": "mock-identity", "key": "mock-key", "import_groups": True, "gateway_id": "mock-gateway-id", }, ) hass.data[tradfri.KEY_GATEWAY] = {entry.entry_id: mock_gateway} hass.data[tradfri.KEY_API] = {entry.entry_id: mock_api} await hass.config_entries.async_forward_entry_setup(entry, "light") def mock_light(test_features={}, test_state={}, n=0): """Mock a tradfri light.""" mock_light_data = Mock(**test_state) mock_light = Mock( id="mock-light-id-{}".format(n), reachable=True, observe=Mock(), device_info=MagicMock(), ) mock_light.name = "tradfri_light_{}".format(n) # Set supported features for the light. features = {**DEFAULT_TEST_FEATURES, **test_features} lc = LightControl(mock_light) for k, v in features.items(): setattr(lc, k, v) # Store the initial state. setattr(lc, "lights", [mock_light_data]) mock_light.light_control = lc return mock_light async def test_light(hass, mock_gateway, mock_api): """Test that lights are correctly added.""" features = {"can_set_dimmer": True, "can_set_color": True, "can_set_temp": True} state = { "state": True, "dimmer": 100, "color_temp": 250, "hsb_xy_color": (100, 100, 100, 100, 100), } mock_gateway.mock_devices.append( mock_light(test_features=features, test_state=state) ) await setup_gateway(hass, mock_gateway, mock_api) lamp_1 = hass.states.get("light.tradfri_light_0") assert lamp_1 is not None assert lamp_1.state == "on" assert lamp_1.attributes["brightness"] == 100 assert lamp_1.attributes["hs_color"] == (0.549, 0.153) async def test_light_observed(hass, mock_gateway, mock_api): """Test that lights are correctly observed.""" light = mock_light() mock_gateway.mock_devices.append(light) await setup_gateway(hass, mock_gateway, mock_api) assert len(light.observe.mock_calls) > 0 async def test_light_available(hass, mock_gateway, mock_api): """Test light available property.""" light = mock_light({"state": True}, n=1) light.reachable = True light2 = mock_light({"state": True}, n=2) light2.reachable = False mock_gateway.mock_devices.append(light) mock_gateway.mock_devices.append(light2) await setup_gateway(hass, mock_gateway, mock_api) assert hass.states.get("light.tradfri_light_1").state == "on" assert hass.states.get("light.tradfri_light_2").state == "unavailable" # Combine TURN_ON_TEST_CASES and TRANSITION_CASES_FOR_TESTS ALL_TURN_ON_TEST_CASES = [["test_features", "test_data", "expected_result", "id"], []] idx = 1 for tc in TURN_ON_TEST_CASES: for trans in TRANSITION_CASES_FOR_TESTS: case = deepcopy(tc) if trans is not None: case[1]["transition"] = trans case.append(idx) idx = idx + 1 ALL_TURN_ON_TEST_CASES[1].append(case) @pytest.mark.parametrize(*ALL_TURN_ON_TEST_CASES) async def test_turn_on( hass, mock_gateway, mock_api, test_features, test_data, expected_result, id ): """Test turning on a light.""" # Note pytradfri style, not hass. Values not really important. initial_state = { "state": False, "dimmer": 0, "color_temp": 250, "hsb_xy_color": (100, 100, 100, 100, 100), } # Setup the gateway with a mock light. light = mock_light(test_features=test_features, test_state=initial_state, n=id) mock_gateway.mock_devices.append(light) await setup_gateway(hass, mock_gateway, mock_api) # Use the turn_on service call to change the light state. await hass.services.async_call( "light", "turn_on", {"entity_id": "light.tradfri_light_{}".format(id), **test_data}, blocking=True, ) await hass.async_block_till_done() # Check that the light is observed. mock_func = light.observe assert len(mock_func.mock_calls) > 0 _, callkwargs = mock_func.call_args assert "callback" in callkwargs # Callback function to refresh light state. cb = callkwargs["callback"] responses = mock_gateway.mock_responses # State on command data. data = {"3311": [{"5850": 1}]} # Add data for all sent commands. for r in responses: data["3311"][0] = {**data["3311"][0], **r["3311"][0]} # Use the callback function to update the light state. dev = Device(data) light_data = Light(dev, 0) light.light_control.lights[0] = light_data cb(light) await hass.async_block_till_done() # Check that the state is correct. states = hass.states.get("light.tradfri_light_{}".format(id)) for k, v in expected_result.items(): if k == "state": assert states.state == v else: # Allow some rounding error in color conversions. assert states.attributes[k] == pytest.approx(v, abs=0.01) async def test_turn_off(hass, mock_gateway, mock_api): """Test turning off a light.""" state = {"state": True, "dimmer": 100} light = mock_light(test_state=state) mock_gateway.mock_devices.append(light) await setup_gateway(hass, mock_gateway, mock_api) # Use the turn_off service call to change the light state. await hass.services.async_call( "light", "turn_off", {"entity_id": "light.tradfri_light_0"}, blocking=True ) await hass.async_block_till_done() # Check that the light is observed. mock_func = light.observe assert len(mock_func.mock_calls) > 0 _, callkwargs = mock_func.call_args assert "callback" in callkwargs # Callback function to refresh light state. cb = callkwargs["callback"] responses = mock_gateway.mock_responses data = {"3311": [{}]} # Add data for all sent commands. for r in responses: data["3311"][0] = {**data["3311"][0], **r["3311"][0]} # Use the callback function to update the light state. dev = Device(data) light_data = Light(dev, 0) light.light_control.lights[0] = light_data cb(light) await hass.async_block_till_done() # Check that the state is correct. states = hass.states.get("light.tradfri_light_0") assert states.state == "off" def mock_group(test_state={}, n=0): """Mock a Tradfri group.""" default_state = {"state": False, "dimmer": 0} state = {**default_state, **test_state} mock_group = Mock(member_ids=[], observe=Mock(), **state) mock_group.name = "tradfri_group_{}".format(n) return mock_group async def test_group(hass, mock_gateway, mock_api): """Test that groups are correctly added.""" mock_gateway.mock_groups.append(mock_group()) state = {"state": True, "dimmer": 100} mock_gateway.mock_groups.append(mock_group(state, 1)) await setup_gateway(hass, mock_gateway, mock_api) group = hass.states.get("light.tradfri_group_0") assert group is not None assert group.state == "off" group = hass.states.get("light.tradfri_group_1") assert group is not None assert group.state == "on" assert group.attributes["brightness"] == 100 async def test_group_turn_on(hass, mock_gateway, mock_api): """Test turning on a group.""" group = mock_group() group2 = mock_group(n=1) group3 = mock_group(n=2) mock_gateway.mock_groups.append(group) mock_gateway.mock_groups.append(group2) mock_gateway.mock_groups.append(group3) await setup_gateway(hass, mock_gateway, mock_api) # Use the turn_off service call to change the light state. await hass.services.async_call( "light", "turn_on", {"entity_id": "light.tradfri_group_0"}, blocking=True ) await hass.services.async_call( "light", "turn_on", {"entity_id": "light.tradfri_group_1", "brightness": 100}, blocking=True, ) await hass.services.async_call( "light", "turn_on", {"entity_id": "light.tradfri_group_2", "brightness": 100, "transition": 1}, blocking=True, ) await hass.async_block_till_done() group.set_state.assert_called_with(1) group2.set_dimmer.assert_called_with(100) group3.set_dimmer.assert_called_with(100, transition_time=10) async def test_group_turn_off(hass, mock_gateway, mock_api): """Test turning off a group.""" group = mock_group({"state": True}) mock_gateway.mock_groups.append(group) await setup_gateway(hass, mock_gateway, mock_api) # Use the turn_off service call to change the light state. await hass.services.async_call( "light", "turn_off", {"entity_id": "light.tradfri_group_0"}, blocking=True ) await hass.async_block_till_done() group.set_state.assert_called_with(0)