diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index e63b8250de82b3..4326715a8f40c0 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -55,6 +55,9 @@ _effect_brightness, _flux_color_mode_to_hass, _hass_color_modes, + _min_rgb_brightness, + _min_rgbw_brightness, + _min_rgbwc_brightness, _str_to_multi_color_effect, ) @@ -281,13 +284,13 @@ def _async_brightness(self, **kwargs: Any) -> int: """Determine brightness from kwargs or current value.""" if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is None: brightness = self.brightness - if not brightness: - # If the brightness was previously 0, the light - # will not turn on unless brightness is at least 1 - # If the device was on and brightness was not - # set, it means it was masked by an effect - brightness = 255 if self.is_on else 1 - return brightness + # If the brightness was previously 0, the light + # will not turn on unless brightness is at least 1 + # + # We previously had a problem with the brightness + # sometimes reporting as 0 when an effect was in progress, + # however this has since been resolved in the upstream library + return max(1, brightness) async def _async_set_mode(self, **kwargs: Any) -> None: """Set an effect or color mode.""" @@ -310,6 +313,8 @@ async def _async_set_mode(self, **kwargs: Any) -> None: return # Handle switch to RGB Color Mode if rgb := kwargs.get(ATTR_RGB_COLOR): + if not self._device.requires_turn_on: + rgb = _min_rgb_brightness(rgb) red, green, blue = rgb await self._device.async_set_levels(red, green, blue, brightness=brightness) return @@ -317,13 +322,18 @@ async def _async_set_mode(self, **kwargs: Any) -> None: if rgbw := kwargs.get(ATTR_RGBW_COLOR): if ATTR_BRIGHTNESS in kwargs: rgbw = rgbw_brightness(rgbw, brightness) + if not self._device.requires_turn_on: + rgbw = _min_rgbw_brightness(rgbw) await self._device.async_set_levels(*rgbw) return # Handle switch to RGBWW Color Mode if rgbcw := kwargs.get(ATTR_RGBWW_COLOR): if ATTR_BRIGHTNESS in kwargs: rgbcw = rgbcw_brightness(kwargs[ATTR_RGBWW_COLOR], brightness) - await self._device.async_set_levels(*rgbcw_to_rgbwc(rgbcw)) + rgbwc = rgbcw_to_rgbwc(rgbcw) + if not self._device.requires_turn_on: + rgbwc = _min_rgbwc_brightness(rgbwc) + await self._device.async_set_levels(*rgbwc) return if (white := kwargs.get(ATTR_WHITE)) is not None: await self._device.async_set_levels(w=white) diff --git a/homeassistant/components/flux_led/util.py b/homeassistant/components/flux_led/util.py index 18be67bf6b4991..da51727fbef39e 100644 --- a/homeassistant/components/flux_led/util.py +++ b/homeassistant/components/flux_led/util.py @@ -52,3 +52,26 @@ def _str_to_multi_color_effect(effect_str: str) -> MultiColorEffects: return effect # unreachable due to schema validation assert False # pragma: no cover + + +def _min_rgb_brightness(rgb: tuple[int, int, int]) -> tuple[int, int, int]: + """Ensure the RGB value will not turn off the device from a turn on command.""" + if all(byte == 0 for byte in rgb): + return (1, 1, 1) + return rgb + + +def _min_rgbw_brightness(rgbw: tuple[int, int, int, int]) -> tuple[int, int, int, int]: + """Ensure the RGBW value will not turn off the device from a turn on command.""" + if all(byte == 0 for byte in rgbw): + return (1, 1, 1, 0) + return rgbw + + +def _min_rgbwc_brightness( + rgbwc: tuple[int, int, int, int, int] +) -> tuple[int, int, int, int, int]: + """Ensure the RGBWC value will not turn off the device from a turn on command.""" + if all(byte == 0 for byte in rgbwc): + return (1, 1, 1, 0, 0) + return rgbwc diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index e3a136afb0b11b..c77cfa956c0710 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -48,6 +48,9 @@ ATTR_RGBWW_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_WHITE, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, DOMAIN as LIGHT_DOMAIN, ) from homeassistant.const import ( @@ -254,9 +257,11 @@ async def test_rgb_light(hass: HomeAssistant) -> None: blocking=True, ) # If the bulb is on and we are using existing brightness - # and brightness was 0 it means we could not read it because - # an effect is in progress so we use 255 - bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=255) + # and brightness was 0 older devices will not be able to turn on + # so we need to make sure its at least 1 and that we + # call it before the turn on command since the device + # does not support auto on + bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=1) bulb.async_set_levels.reset_mock() bulb.brightness = 128 @@ -311,9 +316,9 @@ async def test_rgb_light_auto_on(hass: HomeAssistant) -> None: assert state.state == STATE_ON attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 - assert attributes[ATTR_COLOR_MODE] == "rgb" + assert attributes[ATTR_COLOR_MODE] == COLOR_MODE_RGB assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list - assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_RGB] assert attributes[ATTR_HS_COLOR] == (0, 100) await hass.services.async_call( @@ -338,6 +343,19 @@ async def test_rgb_light_auto_on(hass: HomeAssistant) -> None: bulb.async_set_levels.reset_mock() bulb.async_turn_on.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (0, 0, 0)}, + blocking=True, + ) + # If the bulb is off and we are using existing brightness + # it has to be at least 1 or the bulb won't turn on + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(1, 1, 1, brightness=1) + bulb.async_set_levels.reset_mock() + bulb.async_turn_on.reset_mock() + # Should still be called with no kwargs await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True @@ -364,10 +382,11 @@ async def test_rgb_light_auto_on(hass: HomeAssistant) -> None: blocking=True, ) # If the bulb is on and we are using existing brightness - # and brightness was 0 it means we could not read it because - # an effect is in progress so we use 255 + # and brightness was 0 we need to set it to at least 1 + # or the device may not turn on bulb.async_turn_on.assert_not_called() - bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=255) + bulb.async_set_brightness.assert_not_called() + bulb.async_set_levels.assert_called_with(10, 10, 30, brightness=1) bulb.async_set_levels.reset_mock() bulb.brightness = 128 @@ -402,6 +421,236 @@ async def test_rgb_light_auto_on(hass: HomeAssistant) -> None: bulb.async_set_effect.reset_mock() +async def test_rgbw_light_auto_on(hass: HomeAssistant) -> None: + """Test an rgbw light that does not need the turn on command sent.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.requires_turn_on = False + bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model + bulb.color_modes = {FLUX_COLOR_MODE_RGBW} + bulb.color_mode = FLUX_COLOR_MODE_RGBW + with _patch_discovery(), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.bulb_rgbcw_ddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == COLOR_MODE_RGBW + assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_RGBW] + assert attributes[ATTR_HS_COLOR] == (0.0, 83.529) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_turn_off.assert_called_once() + + await async_mock_device_turn_off(hass, bulb) + assert hass.states.get(entity_id).state == STATE_OFF + + bulb.brightness = 0 + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (10, 10, 30, 0)}, + blocking=True, + ) + # If the bulb is off and we are using existing brightness + # it has to be at least 1 or the bulb won't turn on + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(10, 10, 30, 0) + bulb.async_set_levels.reset_mock() + bulb.async_turn_on.reset_mock() + + # Should still be called with no kwargs + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_turn_on.assert_called_once() + await async_mock_device_turn_on(hass, bulb) + assert hass.states.get(entity_id).state == STATE_ON + bulb.async_turn_on.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_brightness.assert_called_with(100) + bulb.async_set_brightness.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (0, 0, 0, 0)}, + blocking=True, + ) + # If the bulb is on and we are using existing brightness + # and brightness was 0 we need to set it to at least 1 + # or the device may not turn on + bulb.async_turn_on.assert_not_called() + bulb.async_set_brightness.assert_not_called() + bulb.async_set_levels.assert_called_with(1, 1, 1, 0) + bulb.async_set_levels.reset_mock() + + bulb.brightness = 128 + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(110, 19, 0, 255) + bulb.async_set_levels.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_effect.assert_called_once() + bulb.async_set_effect.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_effect.assert_called_with("purple_fade", 50, 50) + bulb.async_set_effect.reset_mock() + + +async def test_rgbww_light_auto_on(hass: HomeAssistant) -> None: + """Test an rgbww light that does not need the turn on command sent.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.requires_turn_on = False + bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model + bulb.color_modes = {FLUX_COLOR_MODE_RGBWW} + bulb.color_mode = FLUX_COLOR_MODE_RGBWW + with _patch_discovery(), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.bulb_rgbcw_ddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == COLOR_MODE_RGBWW + assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_RGBWW] + assert attributes[ATTR_HS_COLOR] == (3.237, 94.51) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_turn_off.assert_called_once() + + await async_mock_device_turn_off(hass, bulb) + assert hass.states.get(entity_id).state == STATE_OFF + + bulb.brightness = 0 + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGBWW_COLOR: (10, 10, 30, 0, 0)}, + blocking=True, + ) + # If the bulb is off and we are using existing brightness + # it has to be at least 1 or the bulb won't turn on + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(10, 10, 30, 0, 0) + bulb.async_set_levels.reset_mock() + bulb.async_turn_on.reset_mock() + + # Should still be called with no kwargs + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_turn_on.assert_called_once() + await async_mock_device_turn_on(hass, bulb) + assert hass.states.get(entity_id).state == STATE_ON + bulb.async_turn_on.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_brightness.assert_called_with(100) + bulb.async_set_brightness.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGBWW_COLOR: (0, 0, 0, 0, 0)}, + blocking=True, + ) + # If the bulb is on and we are using existing brightness + # and brightness was 0 we need to set it to at least 1 + # or the device may not turn on + bulb.async_turn_on.assert_not_called() + bulb.async_set_brightness.assert_not_called() + bulb.async_set_levels.assert_called_with(1, 1, 1, 0, 0) + bulb.async_set_levels.reset_mock() + + bulb.brightness = 128 + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_levels.assert_called_with(14, 0, 30, 255, 255) + bulb.async_set_levels.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_effect.assert_called_once() + bulb.async_set_effect.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, + blocking=True, + ) + bulb.async_turn_on.assert_not_called() + bulb.async_set_effect.assert_called_with("purple_fade", 50, 50) + bulb.async_set_effect.reset_mock() + + async def test_rgb_cct_light(hass: HomeAssistant) -> None: """Test an rgb cct light.""" config_entry = MockConfigEntry(