From 6b2baae0de965bdb2a15c04584260c526f9aebf5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Apr 2020 17:07:39 -0500 Subject: [PATCH] Fix tplink HS220 dimmers (#33909) * HS220 dimmers are handled as lights with a limited feature set --- homeassistant/components/tplink/light.py | 37 ++++- tests/components/tplink/test_light.py | 177 ++++++++++++++++++++++- 2 files changed, 210 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 9ba47c52509..c9d61784d0b 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -345,7 +345,7 @@ class TPLinkSmartBulb(Light): def _get_light_state(self) -> LightState: """Get the light state.""" self._update_emeter() - return self._light_state_from_params(self.smartbulb.get_light_state()) + return self._light_state_from_params(self._get_device_state()) def _update_emeter(self): if not self.smartbulb.has_emeter: @@ -427,7 +427,40 @@ class TPLinkSmartBulb(Light): if not diff: return - return self.smartbulb.set_light_state(diff) + return self._set_device_state(diff) + + def _get_device_state(self): + """State of the bulb or smart dimmer switch.""" + if isinstance(self.smartbulb, SmartBulb): + return self.smartbulb.get_light_state() + + # Its not really a bulb, its a dimmable SmartPlug (aka Wall Switch) + return { + LIGHT_STATE_ON_OFF: self.smartbulb.state, + LIGHT_STATE_BRIGHTNESS: self.smartbulb.brightness, + LIGHT_STATE_COLOR_TEMP: 0, + LIGHT_STATE_HUE: 0, + LIGHT_STATE_SATURATION: 0, + } + + def _set_device_state(self, state): + """Set state of the bulb or smart dimmer switch.""" + if isinstance(self.smartbulb, SmartBulb): + return self.smartbulb.set_light_state(state) + + # Its not really a bulb, its a dimmable SmartPlug (aka Wall Switch) + if LIGHT_STATE_BRIGHTNESS in state: + # Brightness of 0 is accepted by the + # device but the underlying library rejects it + # so we turn off instead. + if state[LIGHT_STATE_BRIGHTNESS]: + self.smartbulb.brightness = state[LIGHT_STATE_BRIGHTNESS] + else: + self.smartbulb.state = 0 + elif LIGHT_STATE_ON_OFF in state: + self.smartbulb.state = state[LIGHT_STATE_ON_OFF] + + return self._get_device_state() def _light_state_diff(old_light_state: LightState, new_light_state: LightState): diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index f6f27a888c5..09c23c6f0e5 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -1,6 +1,6 @@ """Tests for light platform.""" from typing import Callable, NamedTuple -from unittest.mock import Mock, patch +from unittest.mock import Mock, PropertyMock, patch from pyHS100 import SmartDeviceException import pytest @@ -16,7 +16,11 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, DOMAIN as LIGHT_DOMAIN, ) -from homeassistant.components.tplink.common import CONF_DISCOVERY, CONF_LIGHT +from homeassistant.components.tplink.common import ( + CONF_DIMMER, + CONF_DISCOVERY, + CONF_LIGHT, +) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, @@ -41,6 +45,16 @@ class LightMockData(NamedTuple): get_emeter_monthly_mock: Mock +class SmartSwitchMockData(NamedTuple): + """Mock smart switch data.""" + + sys_info: dict + light_state: 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.""" @@ -152,6 +166,75 @@ def light_mock_data_fixture() -> None: ) +@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": ":", + } + + light_state = { + "on_off": 1, + "brightness": 13, + } + + def state(*args, **kwargs): + nonlocal light_state + if len(args) == 0: + return light_state["on_off"] + light_state["on_off"] = args[0] + + def brightness(*args, **kwargs): + nonlocal light_state + if len(args) == 0: + return light_state["brightness"] + if light_state["brightness"] == 0: + light_state["on_off"] = 0 + else: + light_state["on_off"] = 1 + light_state["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, + light_state=light_state, + 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( @@ -160,6 +243,96 @@ async def update_entity(hass: HomeAssistant, entity_id: str) -> None: await hass.async_block_till_done() +async def test_smartswitch( + hass: HomeAssistant, dimmer_switch_mock_data: SmartSwitchMockData +) -> None: + """Test function.""" + light_state = dimmer_switch_mock_data.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_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 light_state["on_off"] == 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"] == 48.45 + assert light_state["on_off"] == 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"] == 53.55 + assert light_state["brightness"] == 21 + + light_state["on_off"] = 0 + light_state["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.3 + assert light_state["brightness"] == 66 + + async def test_light(hass: HomeAssistant, light_mock_data: LightMockData) -> None: """Test function.""" light_state = light_mock_data.light_state