Add color_mode white (#51411)
* Add color_mode white * Include brightness in white parameter * Reformat * Improve test coveragepull/51551/head
parent
50001684aa
commit
e560e623e9
|
@ -61,6 +61,7 @@ COLOR_MODE_XY = "xy"
|
|||
COLOR_MODE_RGB = "rgb"
|
||||
COLOR_MODE_RGBW = "rgbw"
|
||||
COLOR_MODE_RGBWW = "rgbww"
|
||||
COLOR_MODE_WHITE = "white" # Must *NOT* be the only supported mode
|
||||
|
||||
VALID_COLOR_MODES = {
|
||||
COLOR_MODE_ONOFF,
|
||||
|
@ -71,6 +72,7 @@ VALID_COLOR_MODES = {
|
|||
COLOR_MODE_RGB,
|
||||
COLOR_MODE_RGBW,
|
||||
COLOR_MODE_RGBWW,
|
||||
COLOR_MODE_WHITE,
|
||||
}
|
||||
COLOR_MODES_BRIGHTNESS = VALID_COLOR_MODES - {COLOR_MODE_ONOFF}
|
||||
COLOR_MODES_COLOR = {
|
||||
|
@ -90,6 +92,7 @@ def valid_supported_color_modes(color_modes: Iterable[str]) -> set[str]:
|
|||
or COLOR_MODE_UNKNOWN in color_modes
|
||||
or (COLOR_MODE_BRIGHTNESS in color_modes and len(color_modes) > 1)
|
||||
or (COLOR_MODE_ONOFF in color_modes and len(color_modes) > 1)
|
||||
or (COLOR_MODE_WHITE in color_modes and len(color_modes) == 1)
|
||||
):
|
||||
raise vol.Error(f"Invalid supported_color_modes {sorted(color_modes)}")
|
||||
return color_modes
|
||||
|
@ -151,6 +154,7 @@ ATTR_MIN_MIREDS = "min_mireds"
|
|||
ATTR_MAX_MIREDS = "max_mireds"
|
||||
ATTR_COLOR_NAME = "color_name"
|
||||
ATTR_WHITE_VALUE = "white_value"
|
||||
ATTR_WHITE = "white"
|
||||
|
||||
# Brightness of the light, 0..255 or percentage
|
||||
ATTR_BRIGHTNESS = "brightness"
|
||||
|
@ -195,6 +199,19 @@ LIGHT_TURN_ON_SCHEMA = {
|
|||
vol.Exclusive(ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP,
|
||||
vol.Exclusive(ATTR_BRIGHTNESS_STEP_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP_PCT,
|
||||
vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string,
|
||||
vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1)
|
||||
),
|
||||
vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int,
|
||||
vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All(
|
||||
vol.ExactSequence(
|
||||
(
|
||||
vol.All(vol.Coerce(float), vol.Range(min=0, max=360)),
|
||||
vol.All(vol.Coerce(float), vol.Range(min=0, max=100)),
|
||||
)
|
||||
),
|
||||
vol.Coerce(tuple),
|
||||
),
|
||||
vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All(
|
||||
vol.ExactSequence((cv.byte,) * 3), vol.Coerce(tuple)
|
||||
),
|
||||
|
@ -207,19 +224,7 @@ LIGHT_TURN_ON_SCHEMA = {
|
|||
vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All(
|
||||
vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple)
|
||||
),
|
||||
vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All(
|
||||
vol.ExactSequence(
|
||||
(
|
||||
vol.All(vol.Coerce(float), vol.Range(min=0, max=360)),
|
||||
vol.All(vol.Coerce(float), vol.Range(min=0, max=100)),
|
||||
)
|
||||
),
|
||||
vol.Coerce(tuple),
|
||||
),
|
||||
vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1)
|
||||
),
|
||||
vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int,
|
||||
vol.Exclusive(ATTR_WHITE, COLOR_GROUP): VALID_BRIGHTNESS,
|
||||
ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
|
||||
ATTR_FLASH: VALID_FLASH,
|
||||
ATTR_EFFECT: cv.string,
|
||||
|
@ -268,7 +273,7 @@ def preprocess_turn_on_alternatives(hass, params):
|
|||
|
||||
|
||||
def filter_turn_off_params(light, params):
|
||||
"""Filter out params not used in turn off."""
|
||||
"""Filter out params not used in turn off or not supported by the light."""
|
||||
supported_features = light.supported_features
|
||||
|
||||
if not supported_features & SUPPORT_FLASH:
|
||||
|
@ -280,7 +285,7 @@ def filter_turn_off_params(light, params):
|
|||
|
||||
|
||||
def filter_turn_on_params(light, params):
|
||||
"""Filter out params not used in turn off."""
|
||||
"""Filter out params not supported by the light."""
|
||||
supported_features = light.supported_features
|
||||
|
||||
if not supported_features & SUPPORT_EFFECT:
|
||||
|
@ -307,6 +312,8 @@ def filter_turn_on_params(light, params):
|
|||
params.pop(ATTR_RGBW_COLOR, None)
|
||||
if COLOR_MODE_RGBWW not in supported_color_modes:
|
||||
params.pop(ATTR_RGBWW_COLOR, None)
|
||||
if COLOR_MODE_WHITE not in supported_color_modes:
|
||||
params.pop(ATTR_WHITE, None)
|
||||
if COLOR_MODE_XY not in supported_color_modes:
|
||||
params.pop(ATTR_XY_COLOR, None)
|
||||
|
||||
|
@ -427,11 +434,15 @@ async def async_setup(hass, config): # noqa: C901
|
|||
*rgb_color, light.min_mireds, light.max_mireds
|
||||
)
|
||||
|
||||
# If both white and brightness are specified, override white
|
||||
if ATTR_WHITE in params and COLOR_MODE_WHITE in supported_color_modes:
|
||||
params[ATTR_WHITE] = params.pop(ATTR_BRIGHTNESS, params[ATTR_WHITE])
|
||||
|
||||
# Remove deprecated white value if the light supports color mode
|
||||
if supported_color_modes:
|
||||
params.pop(ATTR_WHITE_VALUE, None)
|
||||
|
||||
if params.get(ATTR_BRIGHTNESS) == 0:
|
||||
if params.get(ATTR_BRIGHTNESS) == 0 or params.get(ATTR_WHITE) == 0:
|
||||
await async_handle_light_off_service(light, call)
|
||||
else:
|
||||
await light.async_turn_on(**filter_turn_on_params(light, params))
|
||||
|
|
|
@ -5,7 +5,7 @@ import asyncio
|
|||
from collections.abc import Iterable
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
|
@ -31,6 +31,7 @@ from . import (
|
|||
ATTR_RGBW_COLOR,
|
||||
ATTR_RGBWW_COLOR,
|
||||
ATTR_TRANSITION,
|
||||
ATTR_WHITE,
|
||||
ATTR_WHITE_VALUE,
|
||||
ATTR_XY_COLOR,
|
||||
COLOR_MODE_COLOR_TEMP,
|
||||
|
@ -39,6 +40,7 @@ from . import (
|
|||
COLOR_MODE_RGBW,
|
||||
COLOR_MODE_RGBWW,
|
||||
COLOR_MODE_UNKNOWN,
|
||||
COLOR_MODE_WHITE,
|
||||
COLOR_MODE_XY,
|
||||
DOMAIN,
|
||||
)
|
||||
|
@ -70,12 +72,13 @@ COLOR_GROUP = [
|
|||
]
|
||||
|
||||
COLOR_MODE_TO_ATTRIBUTE = {
|
||||
COLOR_MODE_COLOR_TEMP: ATTR_COLOR_TEMP,
|
||||
COLOR_MODE_HS: ATTR_HS_COLOR,
|
||||
COLOR_MODE_RGB: ATTR_RGB_COLOR,
|
||||
COLOR_MODE_RGBW: ATTR_RGBW_COLOR,
|
||||
COLOR_MODE_RGBWW: ATTR_RGBWW_COLOR,
|
||||
COLOR_MODE_XY: ATTR_XY_COLOR,
|
||||
COLOR_MODE_COLOR_TEMP: (ATTR_COLOR_TEMP, ATTR_COLOR_TEMP),
|
||||
COLOR_MODE_HS: (ATTR_HS_COLOR, ATTR_HS_COLOR),
|
||||
COLOR_MODE_RGB: (ATTR_RGB_COLOR, ATTR_RGB_COLOR),
|
||||
COLOR_MODE_RGBW: (ATTR_RGBW_COLOR, ATTR_RGBW_COLOR),
|
||||
COLOR_MODE_RGBWW: (ATTR_RGBWW_COLOR, ATTR_RGBWW_COLOR),
|
||||
COLOR_MODE_WHITE: (ATTR_WHITE, ATTR_BRIGHTNESS),
|
||||
COLOR_MODE_XY: (ATTR_XY_COLOR, ATTR_XY_COLOR),
|
||||
}
|
||||
|
||||
DEPRECATED_GROUP = [
|
||||
|
@ -93,6 +96,17 @@ DEPRECATION_WARNING = (
|
|||
)
|
||||
|
||||
|
||||
def _color_mode_same(cur_state: State, state: State) -> bool:
|
||||
"""Test if color_mode is same."""
|
||||
cur_color_mode = cur_state.attributes.get(ATTR_COLOR_MODE, COLOR_MODE_UNKNOWN)
|
||||
saved_color_mode = state.attributes.get(ATTR_COLOR_MODE, COLOR_MODE_UNKNOWN)
|
||||
|
||||
# Guard for scenes etc. which where created before color modes were introduced
|
||||
if saved_color_mode == COLOR_MODE_UNKNOWN:
|
||||
return True
|
||||
return cast(bool, cur_color_mode == saved_color_mode)
|
||||
|
||||
|
||||
async def _async_reproduce_state(
|
||||
hass: HomeAssistant,
|
||||
state: State,
|
||||
|
@ -119,9 +133,13 @@ async def _async_reproduce_state(
|
|||
_LOGGER.warning(DEPRECATION_WARNING, deprecated_attrs)
|
||||
|
||||
# Return if we are already at the right state.
|
||||
if cur_state.state == state.state and all(
|
||||
check_attr_equal(cur_state.attributes, state.attributes, attr)
|
||||
for attr in ATTR_GROUP + COLOR_GROUP
|
||||
if (
|
||||
cur_state.state == state.state
|
||||
and _color_mode_same(cur_state, state)
|
||||
and all(
|
||||
check_attr_equal(cur_state.attributes, state.attributes, attr)
|
||||
for attr in ATTR_GROUP + COLOR_GROUP
|
||||
)
|
||||
):
|
||||
return
|
||||
|
||||
|
@ -144,16 +162,17 @@ async def _async_reproduce_state(
|
|||
# Remove deprecated white value if we got a valid color mode
|
||||
service_data.pop(ATTR_WHITE_VALUE, None)
|
||||
color_mode = state.attributes[ATTR_COLOR_MODE]
|
||||
if color_attr := COLOR_MODE_TO_ATTRIBUTE.get(color_mode):
|
||||
if color_attr not in state.attributes:
|
||||
if parameter_state := COLOR_MODE_TO_ATTRIBUTE.get(color_mode):
|
||||
parameter, state_attr = parameter_state
|
||||
if state_attr not in state.attributes:
|
||||
_LOGGER.warning(
|
||||
"Color mode %s specified but attribute %s missing for: %s",
|
||||
color_mode,
|
||||
color_attr,
|
||||
state_attr,
|
||||
state.entity_id,
|
||||
)
|
||||
return
|
||||
service_data[color_attr] = state.attributes[color_attr]
|
||||
service_data[parameter] = state.attributes[state_attr]
|
||||
else:
|
||||
# Fall back to Choosing the first color that is specified
|
||||
for color_attr in COLOR_GROUP:
|
||||
|
|
|
@ -1586,6 +1586,88 @@ async def test_light_service_call_color_conversion(hass, enable_custom_integrati
|
|||
assert data == {"brightness": 128, "rgbww_color": (0, 75, 140, 255, 255)}
|
||||
|
||||
|
||||
async def test_light_service_call_white_mode(hass, enable_custom_integrations):
|
||||
"""Test color_mode white in service calls."""
|
||||
platform = getattr(hass.components, "test.light")
|
||||
platform.init(empty=True)
|
||||
|
||||
platform.ENTITIES.append(platform.MockLight("Test_white", STATE_ON))
|
||||
entity0 = platform.ENTITIES[0]
|
||||
entity0.supported_color_modes = {light.COLOR_MODE_HS, light.COLOR_MODE_WHITE}
|
||||
|
||||
assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity0.entity_id)
|
||||
assert state.attributes["supported_color_modes"] == [
|
||||
light.COLOR_MODE_HS,
|
||||
light.COLOR_MODE_WHITE,
|
||||
]
|
||||
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{
|
||||
"entity_id": [entity0.entity_id],
|
||||
"brightness_pct": 100,
|
||||
"hs_color": (240, 100),
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
_, data = entity0.last_call("turn_on")
|
||||
assert data == {"brightness": 255, "hs_color": (240.0, 100.0)}
|
||||
|
||||
entity0.calls = []
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{"entity_id": [entity0.entity_id], "white": 50},
|
||||
blocking=True,
|
||||
)
|
||||
_, data = entity0.last_call("turn_on")
|
||||
assert data == {"white": 50}
|
||||
|
||||
entity0.calls = []
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{"entity_id": [entity0.entity_id], "white": 0},
|
||||
blocking=True,
|
||||
)
|
||||
_, data = entity0.last_call("turn_off")
|
||||
assert data == {}
|
||||
|
||||
entity0.calls = []
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{"entity_id": [entity0.entity_id], "brightness_pct": 100, "white": 50},
|
||||
blocking=True,
|
||||
)
|
||||
_, data = entity0.last_call("turn_on")
|
||||
assert data == {"white": 255}
|
||||
|
||||
entity0.calls = []
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{"entity_id": [entity0.entity_id], "brightness": 100, "white": 0},
|
||||
blocking=True,
|
||||
)
|
||||
_, data = entity0.last_call("turn_on")
|
||||
assert data == {"white": 100}
|
||||
|
||||
entity0.calls = []
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{"entity_id": [entity0.entity_id], "brightness_pct": 0, "white": 50},
|
||||
blocking=True,
|
||||
)
|
||||
_, data = entity0.last_call("turn_off")
|
||||
assert data == {}
|
||||
|
||||
|
||||
async def test_light_state_color_conversion(hass, enable_custom_integrations):
|
||||
"""Test color conversion in state updates."""
|
||||
platform = getattr(hass.components, "test.light")
|
||||
|
|
|
@ -172,6 +172,7 @@ async def test_reproducing_states(hass, caplog):
|
|||
light.COLOR_MODE_RGBW,
|
||||
light.COLOR_MODE_RGBWW,
|
||||
light.COLOR_MODE_UNKNOWN,
|
||||
light.COLOR_MODE_WHITE,
|
||||
light.COLOR_MODE_XY,
|
||||
),
|
||||
)
|
||||
|
@ -188,6 +189,7 @@ async def test_filter_color_modes(hass, caplog, color_mode):
|
|||
**VALID_RGBW_COLOR,
|
||||
**VALID_RGBWW_COLOR,
|
||||
**VALID_XY_COLOR,
|
||||
**VALID_BRIGHTNESS,
|
||||
}
|
||||
|
||||
turn_on_calls = async_mock_service(hass, "light", "turn_on")
|
||||
|
@ -197,15 +199,23 @@ async def test_filter_color_modes(hass, caplog, color_mode):
|
|||
)
|
||||
|
||||
expected_map = {
|
||||
light.COLOR_MODE_COLOR_TEMP: VALID_COLOR_TEMP,
|
||||
light.COLOR_MODE_BRIGHTNESS: {},
|
||||
light.COLOR_MODE_HS: VALID_HS_COLOR,
|
||||
light.COLOR_MODE_ONOFF: {},
|
||||
light.COLOR_MODE_RGB: VALID_RGB_COLOR,
|
||||
light.COLOR_MODE_RGBW: VALID_RGBW_COLOR,
|
||||
light.COLOR_MODE_RGBWW: VALID_RGBWW_COLOR,
|
||||
light.COLOR_MODE_UNKNOWN: {**VALID_HS_COLOR, **VALID_WHITE_VALUE},
|
||||
light.COLOR_MODE_XY: VALID_XY_COLOR,
|
||||
light.COLOR_MODE_COLOR_TEMP: {**VALID_BRIGHTNESS, **VALID_COLOR_TEMP},
|
||||
light.COLOR_MODE_BRIGHTNESS: VALID_BRIGHTNESS,
|
||||
light.COLOR_MODE_HS: {**VALID_BRIGHTNESS, **VALID_HS_COLOR},
|
||||
light.COLOR_MODE_ONOFF: {**VALID_BRIGHTNESS},
|
||||
light.COLOR_MODE_RGB: {**VALID_BRIGHTNESS, **VALID_RGB_COLOR},
|
||||
light.COLOR_MODE_RGBW: {**VALID_BRIGHTNESS, **VALID_RGBW_COLOR},
|
||||
light.COLOR_MODE_RGBWW: {**VALID_BRIGHTNESS, **VALID_RGBWW_COLOR},
|
||||
light.COLOR_MODE_UNKNOWN: {
|
||||
**VALID_BRIGHTNESS,
|
||||
**VALID_HS_COLOR,
|
||||
**VALID_WHITE_VALUE,
|
||||
},
|
||||
light.COLOR_MODE_WHITE: {
|
||||
**VALID_BRIGHTNESS,
|
||||
light.ATTR_WHITE: VALID_BRIGHTNESS[light.ATTR_BRIGHTNESS],
|
||||
},
|
||||
light.COLOR_MODE_XY: {**VALID_BRIGHTNESS, **VALID_XY_COLOR},
|
||||
}
|
||||
expected = expected_map[color_mode]
|
||||
|
||||
|
@ -213,6 +223,13 @@ async def test_filter_color_modes(hass, caplog, color_mode):
|
|||
assert turn_on_calls[0].domain == "light"
|
||||
assert dict(turn_on_calls[0].data) == {"entity_id": "light.entity", **expected}
|
||||
|
||||
# This should do nothing, the light is already in the desired state
|
||||
hass.states.async_set("light.entity", "on", {"color_mode": color_mode, **expected})
|
||||
await hass.helpers.state.async_reproduce_state(
|
||||
[State("light.entity", "on", {**expected, "color_mode": color_mode})]
|
||||
)
|
||||
assert len(turn_on_calls) == 1
|
||||
|
||||
|
||||
async def test_deprecation_warning(hass, caplog):
|
||||
"""Test deprecation warning."""
|
||||
|
|
Loading…
Reference in New Issue