From 4ae6435a6413932e67ad4492aa8dc3c00dbd0a1a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Aug 2021 17:17:49 -0500 Subject: [PATCH] Avoid increasing yeelight rate limit when the state is already set (#54410) --- homeassistant/components/yeelight/light.py | 35 ++++++- tests/components/yeelight/test_light.py | 112 ++++++++++++++++++++- 2 files changed, 144 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index d2ddc92bb8d..b714ddfaba8 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +import math import voluptuous as vol import yeelight @@ -576,6 +577,13 @@ class YeelightGenericLight(YeelightEntity, LightEntity): async def async_set_brightness(self, brightness, duration) -> None: """Set bulb brightness.""" if brightness: + if math.floor(self.brightness) == math.floor(brightness): + _LOGGER.debug("brightness already set to: %s", brightness) + # Already set, and since we get pushed updates + # we avoid setting it again to ensure we do not + # hit the rate limit + return + _LOGGER.debug("Setting brightness: %s", brightness) await self._bulb.async_set_brightness( brightness / 255 * 100, duration=duration, light_type=self.light_type @@ -585,6 +593,13 @@ class YeelightGenericLight(YeelightEntity, LightEntity): async def async_set_hs(self, hs_color, duration) -> None: """Set bulb's color.""" if hs_color and COLOR_MODE_HS in self.supported_color_modes: + if self.color_mode == COLOR_MODE_HS and self.hs_color == hs_color: + _LOGGER.debug("HS already set to: %s", hs_color) + # Already set, and since we get pushed updates + # we avoid setting it again to ensure we do not + # hit the rate limit + return + _LOGGER.debug("Setting HS: %s", hs_color) await self._bulb.async_set_hsv( hs_color[0], hs_color[1], duration=duration, light_type=self.light_type @@ -594,9 +609,16 @@ class YeelightGenericLight(YeelightEntity, LightEntity): async def async_set_rgb(self, rgb, duration) -> None: """Set bulb's color.""" if rgb and COLOR_MODE_RGB in self.supported_color_modes: + if self.color_mode == COLOR_MODE_RGB and self.rgb_color == rgb: + _LOGGER.debug("RGB already set to: %s", rgb) + # Already set, and since we get pushed updates + # we avoid setting it again to ensure we do not + # hit the rate limit + return + _LOGGER.debug("Setting RGB: %s", rgb) await self._bulb.async_set_rgb( - rgb[0], rgb[1], rgb[2], duration=duration, light_type=self.light_type + *rgb, duration=duration, light_type=self.light_type ) @_async_cmd @@ -604,7 +626,16 @@ class YeelightGenericLight(YeelightEntity, LightEntity): """Set bulb's color temperature.""" if colortemp and COLOR_MODE_COLOR_TEMP in self.supported_color_modes: temp_in_k = mired_to_kelvin(colortemp) - _LOGGER.debug("Setting color temp: %s K", temp_in_k) + + if ( + self.color_mode == COLOR_MODE_COLOR_TEMP + and self.color_temp == colortemp + ): + _LOGGER.debug("Color temp already set to: %s", temp_in_k) + # Already set, and since we get pushed updates + # we avoid setting it again to ensure we do not + # hit the rate limit + return await self._bulb.async_set_color_temp( temp_in_k, duration=duration, light_type=self.light_type diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index 9a1f632242b..8b7ec154b83 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -1,6 +1,6 @@ """Test the Yeelight light.""" import logging -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import ANY, AsyncMock, MagicMock, call, patch from yeelight import ( BulbException, @@ -19,6 +19,7 @@ from yeelight.main import _MODEL_SPECS from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, @@ -428,6 +429,115 @@ async def test_services(hass: HomeAssistant, caplog): ) +async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant): + """Ensure we suppress state changes that will increase the rate limit when there is no change.""" + mocked_bulb = _mocked_bulb() + properties = {**PROPERTIES} + properties.pop("active_mode") + properties["color_mode"] = "3" # HSV + mocked_bulb.last_properties = properties + mocked_bulb.bulb_type = BulbType.Color + config_entry = MockConfigEntry( + domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False} + ) + config_entry.add_to_hass(hass) + with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + # We use asyncio.create_task now to avoid + # blocking starting so we need to block again + await hass.async_block_till_done() + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT, + ATTR_HS_COLOR: (PROPERTIES["hue"], PROPERTIES["sat"]), + }, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [] + + mocked_bulb.last_properties["color_mode"] = 1 + rgb = int(PROPERTIES["rgb"]) + blue = rgb & 0xFF + green = (rgb >> 8) & 0xFF + red = (rgb >> 16) & 0xFF + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_RGB_COLOR: (red, green, blue)}, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [] + mocked_bulb.async_set_rgb.reset_mock() + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT, + ATTR_BRIGHTNESS_PCT: PROPERTIES["current_brightness"], + }, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [] + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_COLOR_TEMP: 250}, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [] + # Should call for the color mode change + assert mocked_bulb.async_set_color_temp.mock_calls == [ + call(4000, duration=350, light_type=ANY) + ] + assert mocked_bulb.async_set_brightness.mock_calls == [] + mocked_bulb.async_set_color_temp.reset_mock() + + mocked_bulb.last_properties["color_mode"] = 2 + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_COLOR_TEMP: 250}, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [] + + mocked_bulb.last_properties["color_mode"] = 3 + # This last change should generate a call even though + # the color mode is the same since the HSV has changed + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_HS_COLOR: (5, 5)}, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [ + call(5.0, 5.0, duration=350, light_type=ANY) + ] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [] + + async def test_device_types(hass: HomeAssistant, caplog): """Test different device types.""" mocked_bulb = _mocked_bulb()