From 9fd6980144c76c36e36690897bfa30e284ccdc55 Mon Sep 17 00:00:00 2001 From: Mario Limonciello Date: Wed, 31 Mar 2021 10:16:24 -0500 Subject: [PATCH] Avoid divide by zero errors in tplink light integration (#48235) --- homeassistant/components/tplink/light.py | 33 +++++- tests/components/tplink/test_light.py | 135 +++++++++++++++++++++++ 2 files changed, 165 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 9a55e644e79..88c24d7cf30 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from datetime import timedelta import logging +import re import time from typing import Any, NamedTuple, cast @@ -60,6 +61,21 @@ LIGHT_SYSINFO_IS_COLOR = "is_color" MAX_ATTEMPTS = 300 SLEEP_TIME = 2 +TPLINK_KELVIN = { + "LB130": (2500, 9000), + "LB120": (2700, 6500), + "LB230": (2500, 9000), + "KB130": (2500, 9000), + "KL130": (2500, 9000), + "KL125": (2500, 6500), + r"KL120\(EU\)": (2700, 6500), + r"KL120\(US\)": (2700, 5000), + r"KL430\(US\)": (2500, 9000), +} + +FALLBACK_MIN_COLOR = 2700 +FALLBACK_MAX_COLOR = 5000 + async def async_setup_entry(hass: HomeAssistantType, config_entry, async_add_entities): """Set up lights.""" @@ -269,6 +285,19 @@ class TPLinkSmartBulb(LightEntity): """Flag supported features.""" return self._light_features.supported_features + def _get_valid_temperature_range(self): + """Return the device-specific white temperature range (in Kelvin). + + :return: White temperature range in Kelvin (minimum, maximum) + """ + model = self.smartbulb.sys_info[LIGHT_SYSINFO_MODEL] + for obj, temp_range in TPLINK_KELVIN.items(): + if re.match(obj, model): + return temp_range + # pyHS100 is abandoned, but some bulb definitions aren't present + # use "safe" values for something that advertises color temperature + return FALLBACK_MIN_COLOR, FALLBACK_MAX_COLOR + def _get_light_features(self): """Determine all supported features in one go.""" sysinfo = self.smartbulb.sys_info @@ -285,9 +314,7 @@ class TPLinkSmartBulb(LightEntity): supported_features += SUPPORT_BRIGHTNESS if sysinfo.get(LIGHT_SYSINFO_IS_VARIABLE_COLOR_TEMP): supported_features += SUPPORT_COLOR_TEMP - # Have to make another api request here in - # order to not re-implement pyHS100 here - max_range, min_range = self.smartbulb.valid_temperature_range + max_range, min_range = self._get_valid_temperature_range() min_mireds = kelvin_to_mired(min_range) max_mireds = kelvin_to_mired(max_range) if sysinfo.get(LIGHT_SYSINFO_IS_COLOR): diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 4c34e754ec3..5e81295468b 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -60,6 +60,116 @@ class SmartSwitchMockData(NamedTuple): get_sysinfo_mock: Mock +@pytest.fixture(name="unknown_light_mock_data") +def unknown_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": "Foo", + "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="light_mock_data") def light_mock_data_fixture() -> None: """Create light mock data.""" @@ -343,6 +453,31 @@ async def test_smartswitch( assert sys_info["brightness"] == 66 +async def test_unknown_light( + hass: HomeAssistant, unknown_light_mock_data: LightMockData +) -> None: + """Test function.""" + 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() + + state = hass.states.get("light.light1") + assert state.state == "on" + assert state.attributes["min_mireds"] == 200 + assert state.attributes["max_mireds"] == 370 + + async def test_light(hass: HomeAssistant, light_mock_data: LightMockData) -> None: """Test function.""" light_state = light_mock_data.light_state