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

This commit is contained in:
J. Nick Koston 2022-01-14 13:14:02 -10:00 committed by Paulus Schoutsen
parent 405c4f685c
commit 4f09439c8c
3 changed files with 308 additions and 22 deletions

View File

@ -71,7 +71,14 @@ from .const import (
TRANSITION_STROBE,
)
from .entity import FluxOnOffEntity
from .util import _effect_brightness, _flux_color_mode_to_hass, _hass_color_modes
from .util import (
_effect_brightness,
_flux_color_mode_to_hass,
_hass_color_modes,
_min_rgb_brightness,
_min_rgbw_brightness,
_min_rgbwc_brightness,
)
_LOGGER = logging.getLogger(__name__)
@ -244,7 +251,7 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity):
@property
def brightness(self) -> int:
"""Return the brightness of this light between 0..255."""
return self._device.brightness
return self._device.brightness # type: ignore
@property
def color_temp(self) -> int:
@ -254,17 +261,17 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity):
@property
def rgb_color(self) -> tuple[int, int, int]:
"""Return the rgb color value."""
return self._device.rgb_unscaled
return self._device.rgb_unscaled # type: ignore
@property
def rgbw_color(self) -> tuple[int, int, int, int]:
"""Return the rgbw color value."""
return self._device.rgbw
return self._device.rgbw # type: ignore
@property
def rgbww_color(self) -> tuple[int, int, int, int, int]:
"""Return the rgbww aka rgbcw color value."""
return self._device.rgbcw
return self._device.rgbcw # type: ignore
@property
def color_mode(self) -> str:
@ -276,7 +283,7 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity):
@property
def effect(self) -> str | None:
"""Return the current effect."""
return self._device.effect
return self._device.effect # type: ignore
async def _async_turn_on(self, **kwargs: Any) -> None:
"""Turn the specified or all lights on."""
@ -313,13 +320,13 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity):
"""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
#
# 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."""
@ -348,6 +355,8 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity):
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
@ -355,13 +364,18 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity):
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)

View File

@ -34,3 +34,26 @@ def _flux_color_mode_to_hass(
def _effect_brightness(brightness: int) -> int:
"""Convert hass brightness to effect brightness."""
return round(brightness / 255 * 100)
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

View File

@ -39,6 +39,9 @@ from homeassistant.components.light import (
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 (
@ -247,9 +250,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
@ -304,9 +309,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(
@ -331,6 +336,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
@ -357,10 +375,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
@ -395,6 +414,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(