Add color_mode white (#51411)

* Add color_mode white

* Include brightness in white parameter

* Reformat

* Improve test coverage
pull/51551/head
Erik Montnemery 2021-06-06 11:13:18 +02:00 committed by GitHub
parent 50001684aa
commit e560e623e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 168 additions and 39 deletions

View File

@ -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))

View File

@ -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:

View File

@ -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")

View File

@ -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."""