"""Tests for light platform.""" import logging from typing import Callable, NamedTuple from pyHS100 import SmartDeviceException import pytest from homeassistant.components import tplink from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, DOMAIN as LIGHT_DOMAIN, ) from homeassistant.components.tplink.common import ( CONF_DIMMER, CONF_DISCOVERY, CONF_LIGHT, ) from homeassistant.components.tplink.light import SLEEP_TIME from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.async_mock import Mock, PropertyMock, patch class LightMockData(NamedTuple): """Mock light data.""" sys_info: dict light_state: dict set_light_state: Callable[[dict], None] set_light_state_mock: Mock get_light_state_mock: Mock current_consumption_mock: Mock get_sysinfo_mock: Mock get_emeter_daily_mock: Mock get_emeter_monthly_mock: Mock class SmartSwitchMockData(NamedTuple): """Mock smart switch data.""" sys_info: dict state_mock: Mock brightness_mock: Mock get_sysinfo_mock: Mock @pytest.fixture(name="light_mock_data") def light_mock_data_fixture() -> None: """Create light mock data.""" sys_info = { "sw_ver": "1.2.3", "hw_ver": "2.3.4", "mac": "aa:bb:cc:dd:ee:ff", "mic_mac": "00:11:22:33:44", "type": "light", "hwId": "1234", "fwId": "4567", "oemId": "891011", "dev_name": "light1", "rssi": 11, "latitude": "0", "longitude": "0", "is_color": True, "is_dimmable": True, "is_variable_color_temp": True, "model": "LB120", "alias": "light1", } light_state = { "on_off": True, "dft_on_state": { "brightness": 12, "color_temp": 3200, "hue": 110, "saturation": 90, }, "brightness": 13, "color_temp": 3300, "hue": 110, "saturation": 90, } def set_light_state(state) -> None: nonlocal light_state drt_on_state = light_state["dft_on_state"] drt_on_state.update(state.get("dft_on_state", {})) light_state.update(state) light_state["dft_on_state"] = drt_on_state return light_state set_light_state_patch = patch( "homeassistant.components.tplink.common.SmartBulb.set_light_state", side_effect=set_light_state, ) get_light_state_patch = patch( "homeassistant.components.tplink.common.SmartBulb.get_light_state", return_value=light_state, ) current_consumption_patch = patch( "homeassistant.components.tplink.common.SmartDevice.current_consumption", return_value=3.23, ) get_sysinfo_patch = patch( "homeassistant.components.tplink.common.SmartDevice.get_sysinfo", return_value=sys_info, ) get_emeter_daily_patch = patch( "homeassistant.components.tplink.common.SmartDevice.get_emeter_daily", return_value={ 1: 1.01, 2: 1.02, 3: 1.03, 4: 1.04, 5: 1.05, 6: 1.06, 7: 1.07, 8: 1.08, 9: 1.09, 10: 1.10, 11: 1.11, 12: 1.12, }, ) get_emeter_monthly_patch = patch( "homeassistant.components.tplink.common.SmartDevice.get_emeter_monthly", return_value={ 1: 2.01, 2: 2.02, 3: 2.03, 4: 2.04, 5: 2.05, 6: 2.06, 7: 2.07, 8: 2.08, 9: 2.09, 10: 2.10, 11: 2.11, 12: 2.12, }, ) with set_light_state_patch as set_light_state_mock, get_light_state_patch as get_light_state_mock, current_consumption_patch as current_consumption_mock, get_sysinfo_patch as get_sysinfo_mock, get_emeter_daily_patch as get_emeter_daily_mock, get_emeter_monthly_patch as get_emeter_monthly_mock: yield LightMockData( sys_info=sys_info, light_state=light_state, set_light_state=set_light_state, set_light_state_mock=set_light_state_mock, get_light_state_mock=get_light_state_mock, current_consumption_mock=current_consumption_mock, get_sysinfo_mock=get_sysinfo_mock, get_emeter_daily_mock=get_emeter_daily_mock, get_emeter_monthly_mock=get_emeter_monthly_mock, ) @pytest.fixture(name="dimmer_switch_mock_data") def dimmer_switch_mock_data_fixture() -> None: """Create dimmer switch mock data.""" sys_info = { "sw_ver": "1.2.3", "hw_ver": "2.3.4", "mac": "aa:bb:cc:dd:ee:ff", "mic_mac": "00:11:22:33:44", "type": "switch", "hwId": "1234", "fwId": "4567", "oemId": "891011", "dev_name": "dimmer1", "rssi": 11, "latitude": "0", "longitude": "0", "is_color": False, "is_dimmable": True, "is_variable_color_temp": False, "model": "HS220", "alias": "dimmer1", "feature": ":", "relay_state": 1, "brightness": 13, } def state(*args, **kwargs): nonlocal sys_info if len(args) == 0: return sys_info["relay_state"] if args[0] == "ON": sys_info["relay_state"] = 1 else: sys_info["relay_state"] = 0 def brightness(*args, **kwargs): nonlocal sys_info if len(args) == 0: return sys_info["brightness"] if sys_info["brightness"] == 0: sys_info["relay_state"] = 0 else: sys_info["relay_state"] = 1 sys_info["brightness"] = args[0] get_sysinfo_patch = patch( "homeassistant.components.tplink.common.SmartDevice.get_sysinfo", return_value=sys_info, ) state_patch = patch( "homeassistant.components.tplink.common.SmartPlug.state", new_callable=PropertyMock, side_effect=state, ) brightness_patch = patch( "homeassistant.components.tplink.common.SmartPlug.brightness", new_callable=PropertyMock, side_effect=brightness, ) with brightness_patch as brightness_mock, state_patch as state_mock, get_sysinfo_patch as get_sysinfo_mock: yield SmartSwitchMockData( sys_info=sys_info, brightness_mock=brightness_mock, state_mock=state_mock, get_sysinfo_mock=get_sysinfo_mock, ) async def update_entity(hass: HomeAssistant, entity_id: str) -> None: """Run an update action for an entity.""" await hass.services.async_call( HA_DOMAIN, SERVICE_UPDATE_ENTITY, {ATTR_ENTITY_ID: entity_id}, blocking=True, ) await hass.async_block_till_done() async def test_smartswitch( hass: HomeAssistant, dimmer_switch_mock_data: SmartSwitchMockData ) -> None: """Test function.""" sys_info = dimmer_switch_mock_data.sys_info await async_setup_component(hass, HA_DOMAIN, {}) await hass.async_block_till_done() await async_setup_component( hass, tplink.DOMAIN, { tplink.DOMAIN: { CONF_DISCOVERY: False, CONF_DIMMER: [{CONF_HOST: "123.123.123.123"}], } }, ) await hass.async_block_till_done() assert hass.states.get("light.dimmer1") await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "light.dimmer1"}, blocking=True, ) await hass.async_block_till_done() await update_entity(hass, "light.dimmer1") assert hass.states.get("light.dimmer1").state == "off" assert sys_info["relay_state"] == 0 await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "light.dimmer1", ATTR_BRIGHTNESS: 50}, blocking=True, ) await hass.async_block_till_done() await update_entity(hass, "light.dimmer1") state = hass.states.get("light.dimmer1") assert state.state == "on" assert state.attributes["brightness"] == 51 assert sys_info["relay_state"] == 1 await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "light.dimmer1", ATTR_BRIGHTNESS: 55}, blocking=True, ) await hass.async_block_till_done() await update_entity(hass, "light.dimmer1") state = hass.states.get("light.dimmer1") assert state.state == "on" assert state.attributes["brightness"] == 56 assert sys_info["brightness"] == 22 sys_info["relay_state"] = 0 sys_info["brightness"] = 66 await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "light.dimmer1"}, blocking=True, ) await hass.async_block_till_done() await update_entity(hass, "light.dimmer1") state = hass.states.get("light.dimmer1") assert state.state == "off" await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "light.dimmer1"}, blocking=True, ) await hass.async_block_till_done() await update_entity(hass, "light.dimmer1") state = hass.states.get("light.dimmer1") assert state.state == "on" assert state.attributes["brightness"] == 168 assert sys_info["brightness"] == 66 async def test_light(hass: HomeAssistant, light_mock_data: LightMockData) -> None: """Test function.""" light_state = light_mock_data.light_state set_light_state = light_mock_data.set_light_state await async_setup_component(hass, HA_DOMAIN, {}) await hass.async_block_till_done() await async_setup_component( hass, tplink.DOMAIN, { tplink.DOMAIN: { CONF_DISCOVERY: False, CONF_LIGHT: [{CONF_HOST: "123.123.123.123"}], } }, ) await hass.async_block_till_done() assert hass.states.get("light.light1") await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "light.light1"}, blocking=True, ) await hass.async_block_till_done() await update_entity(hass, "light.light1") assert hass.states.get("light.light1").state == "off" assert light_state["on_off"] == 0 await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "light.light1", ATTR_COLOR_TEMP: 222, ATTR_BRIGHTNESS: 50}, blocking=True, ) await hass.async_block_till_done() await update_entity(hass, "light.light1") state = hass.states.get("light.light1") assert state.state == "on" assert state.attributes["brightness"] == 51 assert state.attributes["hs_color"] == (110, 90) assert state.attributes["color_temp"] == 222 assert light_state["on_off"] == 1 await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "light.light1", ATTR_BRIGHTNESS: 55, ATTR_HS_COLOR: (23, 27)}, blocking=True, ) await hass.async_block_till_done() await update_entity(hass, "light.light1") state = hass.states.get("light.light1") assert state.state == "on" assert state.attributes["brightness"] == 56 assert state.attributes["hs_color"] == (23, 27) assert light_state["brightness"] == 22 assert light_state["hue"] == 23 assert light_state["saturation"] == 27 light_state["on_off"] = 0 light_state["dft_on_state"]["on_off"] = 0 light_state["brightness"] = 66 light_state["dft_on_state"]["brightness"] = 66 light_state["color_temp"] = 6400 light_state["dft_on_state"]["color_temp"] = 123 light_state["hue"] = 77 light_state["dft_on_state"]["hue"] = 77 light_state["saturation"] = 78 light_state["dft_on_state"]["saturation"] = 78 await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "light.light1"}, blocking=True, ) await hass.async_block_till_done() await update_entity(hass, "light.light1") state = hass.states.get("light.light1") assert state.state == "off" await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "light.light1"}, blocking=True, ) await hass.async_block_till_done() await update_entity(hass, "light.light1") state = hass.states.get("light.light1") assert state.state == "on" assert state.attributes["brightness"] == 168 assert state.attributes["hs_color"] == (77, 78) assert state.attributes["color_temp"] == 156 assert light_state["brightness"] == 66 assert light_state["hue"] == 77 assert light_state["saturation"] == 78 set_light_state({"brightness": 91, "dft_on_state": {"brightness": 91}}) await update_entity(hass, "light.light1") state = hass.states.get("light.light1") assert state.attributes["brightness"] == 232 async def test_get_light_state_retry( hass: HomeAssistant, light_mock_data: LightMockData ) -> None: """Test function.""" # Setup test for retries for sysinfo. get_sysinfo_call_count = 0 def get_sysinfo_side_effect(): nonlocal get_sysinfo_call_count get_sysinfo_call_count += 1 # Need to fail on the 2nd call because the first call is used to # determine if the device is online during the light platform's # setup hook. if get_sysinfo_call_count == 2: raise SmartDeviceException() return light_mock_data.sys_info light_mock_data.get_sysinfo_mock.side_effect = get_sysinfo_side_effect # Setup test for retries of setting state information. set_state_call_count = 0 def set_light_state_side_effect(state_data: dict): nonlocal set_state_call_count, light_mock_data set_state_call_count += 1 if set_state_call_count == 1: raise SmartDeviceException() return light_mock_data.set_light_state(state_data) light_mock_data.set_light_state_mock.side_effect = set_light_state_side_effect # Setup component. await async_setup_component(hass, HA_DOMAIN, {}) await hass.async_block_till_done() await async_setup_component( hass, tplink.DOMAIN, { tplink.DOMAIN: { CONF_DISCOVERY: False, CONF_LIGHT: [{CONF_HOST: "123.123.123.123"}], } }, ) await hass.async_block_till_done() await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "light.light1"}, blocking=True, ) await hass.async_block_till_done() await update_entity(hass, "light.light1") assert light_mock_data.get_sysinfo_mock.call_count > 1 assert light_mock_data.get_light_state_mock.call_count > 1 assert light_mock_data.set_light_state_mock.call_count > 1 assert light_mock_data.get_sysinfo_mock.call_count < 40 assert light_mock_data.get_light_state_mock.call_count < 40 assert light_mock_data.set_light_state_mock.call_count < 10 async def test_update_failure( hass: HomeAssistant, light_mock_data: LightMockData, caplog ): """Test that update failures are logged.""" await async_setup_component(hass, HA_DOMAIN, {}) await hass.async_block_till_done() await async_setup_component( hass, tplink.DOMAIN, { tplink.DOMAIN: { CONF_DISCOVERY: False, CONF_LIGHT: [{CONF_HOST: "123.123.123.123"}], } }, ) await hass.async_block_till_done() caplog.clear() caplog.set_level(logging.WARNING) await hass.helpers.entity_component.async_update_entity("light.light1") assert caplog.text == "" with patch("homeassistant.components.tplink.light.MAX_ATTEMPTS", 0): caplog.clear() caplog.set_level(logging.WARNING) await hass.helpers.entity_component.async_update_entity("light.light1") assert "Could not read state for 123.123.123.123|light1" in caplog.text get_state_call_count = 0 def get_light_state_side_effect(): nonlocal get_state_call_count get_state_call_count += 1 if get_state_call_count == 1: raise SmartDeviceException() return light_mock_data.light_state light_mock_data.get_light_state_mock.side_effect = get_light_state_side_effect with patch("homeassistant.components.tplink.light", MAX_ATTEMPTS=2, SLEEP_TIME=0): caplog.clear() caplog.set_level(logging.DEBUG) await update_entity(hass, "light.light1") assert ( f"Retrying in {SLEEP_TIME} seconds for 123.123.123.123|light1" in caplog.text ) assert "Device 123.123.123.123|light1 responded after " in caplog.text