Add color_mode white (#51411)

* Add color_mode white

* Include brightness in white parameter

* Reformat

* Improve test coverage
This commit is contained in:
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_RGB = "rgb"
COLOR_MODE_RGBW = "rgbw" COLOR_MODE_RGBW = "rgbw"
COLOR_MODE_RGBWW = "rgbww" COLOR_MODE_RGBWW = "rgbww"
COLOR_MODE_WHITE = "white" # Must *NOT* be the only supported mode
VALID_COLOR_MODES = { VALID_COLOR_MODES = {
COLOR_MODE_ONOFF, COLOR_MODE_ONOFF,
@ -71,6 +72,7 @@ VALID_COLOR_MODES = {
COLOR_MODE_RGB, COLOR_MODE_RGB,
COLOR_MODE_RGBW, COLOR_MODE_RGBW,
COLOR_MODE_RGBWW, COLOR_MODE_RGBWW,
COLOR_MODE_WHITE,
} }
COLOR_MODES_BRIGHTNESS = VALID_COLOR_MODES - {COLOR_MODE_ONOFF} COLOR_MODES_BRIGHTNESS = VALID_COLOR_MODES - {COLOR_MODE_ONOFF}
COLOR_MODES_COLOR = { 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_UNKNOWN in color_modes
or (COLOR_MODE_BRIGHTNESS in color_modes and len(color_modes) > 1) 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_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)}") raise vol.Error(f"Invalid supported_color_modes {sorted(color_modes)}")
return color_modes return color_modes
@ -151,6 +154,7 @@ ATTR_MIN_MIREDS = "min_mireds"
ATTR_MAX_MIREDS = "max_mireds" ATTR_MAX_MIREDS = "max_mireds"
ATTR_COLOR_NAME = "color_name" ATTR_COLOR_NAME = "color_name"
ATTR_WHITE_VALUE = "white_value" ATTR_WHITE_VALUE = "white_value"
ATTR_WHITE = "white"
# Brightness of the light, 0..255 or percentage # Brightness of the light, 0..255 or percentage
ATTR_BRIGHTNESS = "brightness" 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, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP,
vol.Exclusive(ATTR_BRIGHTNESS_STEP_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP_PCT, 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_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.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All(
vol.ExactSequence((cv.byte,) * 3), vol.Coerce(tuple) 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.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All(
vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple) vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple)
), ),
vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( vol.Exclusive(ATTR_WHITE, COLOR_GROUP): VALID_BRIGHTNESS,
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,
ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
ATTR_FLASH: VALID_FLASH, ATTR_FLASH: VALID_FLASH,
ATTR_EFFECT: cv.string, ATTR_EFFECT: cv.string,
@ -268,7 +273,7 @@ def preprocess_turn_on_alternatives(hass, params):
def filter_turn_off_params(light, 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 supported_features = light.supported_features
if not supported_features & SUPPORT_FLASH: if not supported_features & SUPPORT_FLASH:
@ -280,7 +285,7 @@ def filter_turn_off_params(light, params):
def filter_turn_on_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 supported_features = light.supported_features
if not supported_features & SUPPORT_EFFECT: if not supported_features & SUPPORT_EFFECT:
@ -307,6 +312,8 @@ def filter_turn_on_params(light, params):
params.pop(ATTR_RGBW_COLOR, None) params.pop(ATTR_RGBW_COLOR, None)
if COLOR_MODE_RGBWW not in supported_color_modes: if COLOR_MODE_RGBWW not in supported_color_modes:
params.pop(ATTR_RGBWW_COLOR, None) 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: if COLOR_MODE_XY not in supported_color_modes:
params.pop(ATTR_XY_COLOR, None) 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 *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 # Remove deprecated white value if the light supports color mode
if supported_color_modes: if supported_color_modes:
params.pop(ATTR_WHITE_VALUE, None) 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) await async_handle_light_off_service(light, call)
else: else:
await light.async_turn_on(**filter_turn_on_params(light, params)) await light.async_turn_on(**filter_turn_on_params(light, params))

View File

@ -5,7 +5,7 @@ import asyncio
from collections.abc import Iterable from collections.abc import Iterable
import logging import logging
from types import MappingProxyType from types import MappingProxyType
from typing import Any from typing import Any, cast
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
@ -31,6 +31,7 @@ from . import (
ATTR_RGBW_COLOR, ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR, ATTR_RGBWW_COLOR,
ATTR_TRANSITION, ATTR_TRANSITION,
ATTR_WHITE,
ATTR_WHITE_VALUE, ATTR_WHITE_VALUE,
ATTR_XY_COLOR, ATTR_XY_COLOR,
COLOR_MODE_COLOR_TEMP, COLOR_MODE_COLOR_TEMP,
@ -39,6 +40,7 @@ from . import (
COLOR_MODE_RGBW, COLOR_MODE_RGBW,
COLOR_MODE_RGBWW, COLOR_MODE_RGBWW,
COLOR_MODE_UNKNOWN, COLOR_MODE_UNKNOWN,
COLOR_MODE_WHITE,
COLOR_MODE_XY, COLOR_MODE_XY,
DOMAIN, DOMAIN,
) )
@ -70,12 +72,13 @@ COLOR_GROUP = [
] ]
COLOR_MODE_TO_ATTRIBUTE = { COLOR_MODE_TO_ATTRIBUTE = {
COLOR_MODE_COLOR_TEMP: ATTR_COLOR_TEMP, COLOR_MODE_COLOR_TEMP: (ATTR_COLOR_TEMP, ATTR_COLOR_TEMP),
COLOR_MODE_HS: ATTR_HS_COLOR, COLOR_MODE_HS: (ATTR_HS_COLOR, ATTR_HS_COLOR),
COLOR_MODE_RGB: ATTR_RGB_COLOR, COLOR_MODE_RGB: (ATTR_RGB_COLOR, ATTR_RGB_COLOR),
COLOR_MODE_RGBW: ATTR_RGBW_COLOR, COLOR_MODE_RGBW: (ATTR_RGBW_COLOR, ATTR_RGBW_COLOR),
COLOR_MODE_RGBWW: ATTR_RGBWW_COLOR, COLOR_MODE_RGBWW: (ATTR_RGBWW_COLOR, ATTR_RGBWW_COLOR),
COLOR_MODE_XY: ATTR_XY_COLOR, COLOR_MODE_WHITE: (ATTR_WHITE, ATTR_BRIGHTNESS),
COLOR_MODE_XY: (ATTR_XY_COLOR, ATTR_XY_COLOR),
} }
DEPRECATED_GROUP = [ 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( async def _async_reproduce_state(
hass: HomeAssistant, hass: HomeAssistant,
state: State, state: State,
@ -119,9 +133,13 @@ async def _async_reproduce_state(
_LOGGER.warning(DEPRECATION_WARNING, deprecated_attrs) _LOGGER.warning(DEPRECATION_WARNING, deprecated_attrs)
# Return if we are already at the right state. # Return if we are already at the right state.
if cur_state.state == state.state and all( if (
cur_state.state == state.state
and _color_mode_same(cur_state, state)
and all(
check_attr_equal(cur_state.attributes, state.attributes, attr) check_attr_equal(cur_state.attributes, state.attributes, attr)
for attr in ATTR_GROUP + COLOR_GROUP for attr in ATTR_GROUP + COLOR_GROUP
)
): ):
return return
@ -144,16 +162,17 @@ async def _async_reproduce_state(
# Remove deprecated white value if we got a valid color mode # Remove deprecated white value if we got a valid color mode
service_data.pop(ATTR_WHITE_VALUE, None) service_data.pop(ATTR_WHITE_VALUE, None)
color_mode = state.attributes[ATTR_COLOR_MODE] color_mode = state.attributes[ATTR_COLOR_MODE]
if color_attr := COLOR_MODE_TO_ATTRIBUTE.get(color_mode): if parameter_state := COLOR_MODE_TO_ATTRIBUTE.get(color_mode):
if color_attr not in state.attributes: parameter, state_attr = parameter_state
if state_attr not in state.attributes:
_LOGGER.warning( _LOGGER.warning(
"Color mode %s specified but attribute %s missing for: %s", "Color mode %s specified but attribute %s missing for: %s",
color_mode, color_mode,
color_attr, state_attr,
state.entity_id, state.entity_id,
) )
return return
service_data[color_attr] = state.attributes[color_attr] service_data[parameter] = state.attributes[state_attr]
else: else:
# Fall back to Choosing the first color that is specified # Fall back to Choosing the first color that is specified
for color_attr in COLOR_GROUP: 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)} 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): async def test_light_state_color_conversion(hass, enable_custom_integrations):
"""Test color conversion in state updates.""" """Test color conversion in state updates."""
platform = getattr(hass.components, "test.light") 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_RGBW,
light.COLOR_MODE_RGBWW, light.COLOR_MODE_RGBWW,
light.COLOR_MODE_UNKNOWN, light.COLOR_MODE_UNKNOWN,
light.COLOR_MODE_WHITE,
light.COLOR_MODE_XY, light.COLOR_MODE_XY,
), ),
) )
@ -188,6 +189,7 @@ async def test_filter_color_modes(hass, caplog, color_mode):
**VALID_RGBW_COLOR, **VALID_RGBW_COLOR,
**VALID_RGBWW_COLOR, **VALID_RGBWW_COLOR,
**VALID_XY_COLOR, **VALID_XY_COLOR,
**VALID_BRIGHTNESS,
} }
turn_on_calls = async_mock_service(hass, "light", "turn_on") 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 = { expected_map = {
light.COLOR_MODE_COLOR_TEMP: VALID_COLOR_TEMP, light.COLOR_MODE_COLOR_TEMP: {**VALID_BRIGHTNESS, **VALID_COLOR_TEMP},
light.COLOR_MODE_BRIGHTNESS: {}, light.COLOR_MODE_BRIGHTNESS: VALID_BRIGHTNESS,
light.COLOR_MODE_HS: VALID_HS_COLOR, light.COLOR_MODE_HS: {**VALID_BRIGHTNESS, **VALID_HS_COLOR},
light.COLOR_MODE_ONOFF: {}, light.COLOR_MODE_ONOFF: {**VALID_BRIGHTNESS},
light.COLOR_MODE_RGB: VALID_RGB_COLOR, light.COLOR_MODE_RGB: {**VALID_BRIGHTNESS, **VALID_RGB_COLOR},
light.COLOR_MODE_RGBW: VALID_RGBW_COLOR, light.COLOR_MODE_RGBW: {**VALID_BRIGHTNESS, **VALID_RGBW_COLOR},
light.COLOR_MODE_RGBWW: VALID_RGBWW_COLOR, light.COLOR_MODE_RGBWW: {**VALID_BRIGHTNESS, **VALID_RGBWW_COLOR},
light.COLOR_MODE_UNKNOWN: {**VALID_HS_COLOR, **VALID_WHITE_VALUE}, light.COLOR_MODE_UNKNOWN: {
light.COLOR_MODE_XY: VALID_XY_COLOR, **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] 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 turn_on_calls[0].domain == "light"
assert dict(turn_on_calls[0].data) == {"entity_id": "light.entity", **expected} 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): async def test_deprecation_warning(hass, caplog):
"""Test deprecation warning.""" """Test deprecation warning."""