Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix flux_led turn on when brightness is zero on newer devices #64129

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 18 additions & 8 deletions homeassistant/components/flux_led/light.py
Expand Up @@ -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,
)

Expand Down Expand Up @@ -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."""
Expand All @@ -310,20 +313,27 @@ 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
# Handle switch to RGBW Color Mode
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)
Expand Down
23 changes: 23 additions & 0 deletions homeassistant/components/flux_led/util.py
Expand Up @@ -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
265 changes: 257 additions & 8 deletions tests/components/flux_led/test_light.py
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down