Fix loss of ability to control white channel in HomeKit on RGB&W lights (#65864)

* Fix loss of ability to control white channel in HomeKit on RGB&W lights

- Fix white channel missing from RGB/W lights

- Fix temp missing from RGB/CW lights

- Fixes #65529

* cover the missing case

* bright fix

* force brightness notify on color mode change as well
pull/65956/head
J. Nick Koston 2022-02-06 16:12:30 -06:00 committed by GitHub
parent b1dcf7e0d8
commit 41f602c3df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 522 additions and 92 deletions

View File

@ -1,4 +1,6 @@
"""Class to hold all light accessories.""" """Class to hold all light accessories."""
from __future__ import annotations
import logging import logging
import math import math
@ -12,12 +14,13 @@ from homeassistant.components.light import (
ATTR_HS_COLOR, ATTR_HS_COLOR,
ATTR_MAX_MIREDS, ATTR_MAX_MIREDS,
ATTR_MIN_MIREDS, ATTR_MIN_MIREDS,
ATTR_RGB_COLOR,
ATTR_RGBW_COLOR, ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR, ATTR_RGBWW_COLOR,
ATTR_SUPPORTED_COLOR_MODES, ATTR_SUPPORTED_COLOR_MODES,
ATTR_WHITE,
COLOR_MODE_RGBW, COLOR_MODE_RGBW,
COLOR_MODE_RGBWW, COLOR_MODE_RGBWW,
COLOR_MODE_WHITE,
DOMAIN, DOMAIN,
brightness_supported, brightness_supported,
color_supported, color_supported,
@ -32,9 +35,9 @@ from homeassistant.const import (
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
from homeassistant.util.color import ( from homeassistant.util.color import (
color_hsv_to_RGB,
color_temperature_mired_to_kelvin, color_temperature_mired_to_kelvin,
color_temperature_to_hs, color_temperature_to_hs,
color_temperature_to_rgbww,
) )
from .accessories import TYPES, HomeAccessory from .accessories import TYPES, HomeAccessory
@ -51,12 +54,13 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
RGB_COLOR = "rgb_color"
CHANGE_COALESCE_TIME_WINDOW = 0.01 CHANGE_COALESCE_TIME_WINDOW = 0.01
DEFAULT_MIN_MIREDS = 153
DEFAULT_MAX_MIREDS = 500
COLOR_MODES_WITH_WHITES = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW} COLOR_MODES_WITH_WHITES = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW, COLOR_MODE_WHITE}
@TYPES.register("Light") @TYPES.register("Light")
@ -79,8 +83,12 @@ class Light(HomeAccessory):
self.color_modes = color_modes = ( self.color_modes = color_modes = (
attributes.get(ATTR_SUPPORTED_COLOR_MODES) or [] attributes.get(ATTR_SUPPORTED_COLOR_MODES) or []
) )
self._previous_color_mode = attributes.get(ATTR_COLOR_MODE)
self.color_supported = color_supported(color_modes) self.color_supported = color_supported(color_modes)
self.color_temp_supported = color_temp_supported(color_modes) self.color_temp_supported = color_temp_supported(color_modes)
self.rgbw_supported = COLOR_MODE_RGBW in color_modes
self.rgbww_supported = COLOR_MODE_RGBWW in color_modes
self.white_supported = COLOR_MODE_WHITE in color_modes
self.brightness_supported = brightness_supported(color_modes) self.brightness_supported = brightness_supported(color_modes)
if self.brightness_supported: if self.brightness_supported:
@ -89,7 +97,9 @@ class Light(HomeAccessory):
if self.color_supported: if self.color_supported:
self.chars.extend([CHAR_HUE, CHAR_SATURATION]) self.chars.extend([CHAR_HUE, CHAR_SATURATION])
if self.color_temp_supported: if self.color_temp_supported or COLOR_MODES_WITH_WHITES.intersection(
self.color_modes
):
self.chars.append(CHAR_COLOR_TEMPERATURE) self.chars.append(CHAR_COLOR_TEMPERATURE)
serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars)
@ -101,13 +111,22 @@ class Light(HomeAccessory):
# to set to the correct initial value. # to set to the correct initial value.
self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100) self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100)
if self.color_temp_supported: if CHAR_COLOR_TEMPERATURE in self.chars:
min_mireds = math.floor(attributes.get(ATTR_MIN_MIREDS, 153)) self.min_mireds = math.floor(
max_mireds = math.ceil(attributes.get(ATTR_MAX_MIREDS, 500)) attributes.get(ATTR_MIN_MIREDS, DEFAULT_MIN_MIREDS)
)
self.max_mireds = math.ceil(
attributes.get(ATTR_MAX_MIREDS, DEFAULT_MAX_MIREDS)
)
if not self.color_temp_supported and not self.rgbww_supported:
self.max_mireds = self.min_mireds
self.char_color_temp = serv_light.configure_char( self.char_color_temp = serv_light.configure_char(
CHAR_COLOR_TEMPERATURE, CHAR_COLOR_TEMPERATURE,
value=min_mireds, value=self.min_mireds,
properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds}, properties={
PROP_MIN_VALUE: self.min_mireds,
PROP_MAX_VALUE: self.max_mireds,
},
) )
if self.color_supported: if self.color_supported:
@ -165,33 +184,32 @@ class Light(HomeAccessory):
) )
return return
# Handle white channels
if CHAR_COLOR_TEMPERATURE in char_values: if CHAR_COLOR_TEMPERATURE in char_values:
params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE] temp = char_values[CHAR_COLOR_TEMPERATURE]
events.append(f"color temperature at {params[ATTR_COLOR_TEMP]}") events.append(f"color temperature at {temp}")
bright_val = round(
((brightness_pct or self.char_brightness.value) * 255) / 100
)
if self.color_temp_supported:
params[ATTR_COLOR_TEMP] = temp
elif self.rgbww_supported:
params[ATTR_RGBWW_COLOR] = color_temperature_to_rgbww(
temp, bright_val, self.min_mireds, self.max_mireds
)
elif self.rgbw_supported:
params[ATTR_RGBW_COLOR] = (*(0,) * 3, bright_val)
elif self.white_supported:
params[ATTR_WHITE] = bright_val
elif ( elif CHAR_HUE in char_values or CHAR_SATURATION in char_values:
CHAR_HUE in char_values
or CHAR_SATURATION in char_values
# If we are adjusting brightness we need to send the full RGBW/RGBWW values
# since HomeKit does not support RGBW/RGBWW
or brightness_pct
and COLOR_MODES_WITH_WHITES.intersection(self.color_modes)
):
hue_sat = ( hue_sat = (
char_values.get(CHAR_HUE, self.char_hue.value), char_values.get(CHAR_HUE, self.char_hue.value),
char_values.get(CHAR_SATURATION, self.char_saturation.value), char_values.get(CHAR_SATURATION, self.char_saturation.value),
) )
_LOGGER.debug("%s: Set hs_color to %s", self.entity_id, hue_sat) _LOGGER.debug("%s: Set hs_color to %s", self.entity_id, hue_sat)
events.append(f"set color at {hue_sat}") events.append(f"set color at {hue_sat}")
# HomeKit doesn't support RGBW/RGBWW so we need to remove any white values params[ATTR_HS_COLOR] = hue_sat
if COLOR_MODE_RGBWW in self.color_modes:
val = brightness_pct or self.char_brightness.value
params[ATTR_RGBWW_COLOR] = (*color_hsv_to_RGB(*hue_sat, val), 0, 0)
elif COLOR_MODE_RGBW in self.color_modes:
val = brightness_pct or self.char_brightness.value
params[ATTR_RGBW_COLOR] = (*color_hsv_to_RGB(*hue_sat, val), 0)
else:
params[ATTR_HS_COLOR] = hue_sat
if ( if (
brightness_pct brightness_pct
@ -200,6 +218,9 @@ class Light(HomeAccessory):
): ):
params[ATTR_BRIGHTNESS_PCT] = brightness_pct params[ATTR_BRIGHTNESS_PCT] = brightness_pct
_LOGGER.debug(
"Calling light service with params: %s -> %s", char_values, params
)
self.async_call_service(DOMAIN, service, params, ", ".join(events)) self.async_call_service(DOMAIN, service, params, ", ".join(events))
@callback @callback
@ -210,52 +231,59 @@ class Light(HomeAccessory):
attributes = new_state.attributes attributes = new_state.attributes
color_mode = attributes.get(ATTR_COLOR_MODE) color_mode = attributes.get(ATTR_COLOR_MODE)
self.char_on.set_value(int(state == STATE_ON)) self.char_on.set_value(int(state == STATE_ON))
color_mode_changed = self._previous_color_mode != color_mode
self._previous_color_mode = color_mode
# Handle Brightness # Handle Brightness
if self.brightness_supported: if (
if ( self.brightness_supported
color_mode and (brightness := attributes.get(ATTR_BRIGHTNESS)) is not None
and COLOR_MODES_WITH_WHITES.intersection({color_mode}) and isinstance(brightness, (int, float))
and (rgb_color := attributes.get(ATTR_RGB_COLOR)) ):
): brightness = round(brightness / 255 * 100, 0)
# HomeKit doesn't support RGBW/RGBWW so we need to # The homeassistant component might report its brightness as 0 but is
# give it the color brightness only # not off. But 0 is a special value in homekit. When you turn on a
brightness = max(rgb_color) # homekit accessory it will try to restore the last brightness state
else: # which will be the last value saved by char_brightness.set_value.
brightness = attributes.get(ATTR_BRIGHTNESS) # But if it is set to 0, HomeKit will update the brightness to 100 as
if isinstance(brightness, (int, float)): # it thinks 0 is off.
brightness = round(brightness / 255 * 100, 0) #
# The homeassistant component might report its brightness as 0 but is # Therefore, if the the brightness is 0 and the device is still on,
# not off. But 0 is a special value in homekit. When you turn on a # the brightness is mapped to 1 otherwise the update is ignored in
# homekit accessory it will try to restore the last brightness state # order to avoid this incorrect behavior.
# which will be the last value saved by char_brightness.set_value. if brightness == 0 and state == STATE_ON:
# But if it is set to 0, HomeKit will update the brightness to 100 as brightness = 1
# it thinks 0 is off. self.char_brightness.set_value(brightness)
# if color_mode_changed:
# Therefore, if the the brightness is 0 and the device is still on, self.char_brightness.notify()
# the brightness is mapped to 1 otherwise the update is ignored in
# order to avoid this incorrect behavior.
if brightness == 0 and state == STATE_ON:
brightness = 1
self.char_brightness.set_value(brightness)
# Handle Color - color must always be set before color temperature # Handle Color - color must always be set before color temperature
# or the iOS UI will not display it correctly. # or the iOS UI will not display it correctly.
if self.color_supported: if self.color_supported:
if ATTR_COLOR_TEMP in attributes: if color_temp := attributes.get(ATTR_COLOR_TEMP):
hue, saturation = color_temperature_to_hs( hue, saturation = color_temperature_to_hs(
color_temperature_mired_to_kelvin( color_temperature_mired_to_kelvin(color_temp)
new_state.attributes[ATTR_COLOR_TEMP]
)
) )
elif color_mode == COLOR_MODE_WHITE:
hue, saturation = 0, 0
else: else:
hue, saturation = attributes.get(ATTR_HS_COLOR, (None, None)) hue, saturation = attributes.get(ATTR_HS_COLOR, (None, None))
if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)): if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)):
self.char_hue.set_value(round(hue, 0)) self.char_hue.set_value(round(hue, 0))
self.char_saturation.set_value(round(saturation, 0)) self.char_saturation.set_value(round(saturation, 0))
if color_mode_changed:
# If the color temp changed, be sure to force the color to update
self.char_hue.notify()
self.char_saturation.notify()
# Handle color temperature # Handle white channels
if self.color_temp_supported: if CHAR_COLOR_TEMPERATURE in self.chars:
color_temp = attributes.get(ATTR_COLOR_TEMP) color_temp = None
if self.color_temp_supported:
color_temp = attributes.get(ATTR_COLOR_TEMP)
elif color_mode == COLOR_MODE_WHITE:
color_temp = self.min_mireds
if isinstance(color_temp, (int, float)): if isinstance(color_temp, (int, float)):
self.char_color_temp.set_value(round(color_temp, 0)) self.char_color_temp.set_value(round(color_temp, 0))
if color_mode_changed:
self.char_color_temp.notify()

View File

@ -5,7 +5,11 @@ from datetime import timedelta
from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE
import pytest import pytest
from homeassistant.components.homekit.const import ATTR_VALUE from homeassistant.components.homekit.const import (
ATTR_VALUE,
PROP_MAX_VALUE,
PROP_MIN_VALUE,
)
from homeassistant.components.homekit.type_lights import ( from homeassistant.components.homekit.type_lights import (
CHANGE_COALESCE_TIME_WINDOW, CHANGE_COALESCE_TIME_WINDOW,
Light, Light,
@ -22,9 +26,12 @@ from homeassistant.components.light import (
ATTR_RGBW_COLOR, ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR, ATTR_RGBWW_COLOR,
ATTR_SUPPORTED_COLOR_MODES, ATTR_SUPPORTED_COLOR_MODES,
ATTR_WHITE,
COLOR_MODE_COLOR_TEMP, COLOR_MODE_COLOR_TEMP,
COLOR_MODE_RGB,
COLOR_MODE_RGBW, COLOR_MODE_RGBW,
COLOR_MODE_RGBWW, COLOR_MODE_RGBWW,
COLOR_MODE_WHITE,
DOMAIN, DOMAIN,
) )
from homeassistant.const import ( from homeassistant.const import (
@ -573,7 +580,7 @@ async def test_light_restore(hass, hk_driver, events):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"supported_color_modes, state_props, turn_on_props, turn_on_props_with_brightness", "supported_color_modes, state_props, turn_on_props_with_brightness",
[ [
[ [
[COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBW], [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBW],
@ -584,8 +591,7 @@ async def test_light_restore(hass, hk_driver, events):
ATTR_BRIGHTNESS: 255, ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBW, ATTR_COLOR_MODE: COLOR_MODE_RGBW,
}, },
{ATTR_RGBW_COLOR: (31, 127, 71, 0)}, {ATTR_HS_COLOR: (145, 75), ATTR_BRIGHTNESS_PCT: 25},
{ATTR_RGBW_COLOR: (15, 63, 35, 0)},
], ],
[ [
[COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBWW], [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBWW],
@ -596,21 +602,19 @@ async def test_light_restore(hass, hk_driver, events):
ATTR_BRIGHTNESS: 255, ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBWW, ATTR_COLOR_MODE: COLOR_MODE_RGBWW,
}, },
{ATTR_RGBWW_COLOR: (31, 127, 71, 0, 0)}, {ATTR_HS_COLOR: (145, 75), ATTR_BRIGHTNESS_PCT: 25},
{ATTR_RGBWW_COLOR: (15, 63, 35, 0, 0)},
], ],
], ],
) )
async def test_light_rgb_with_white( async def test_light_rgb_with_color_temp(
hass, hass,
hk_driver, hk_driver,
events, events,
supported_color_modes, supported_color_modes,
state_props, state_props,
turn_on_props,
turn_on_props_with_brightness, turn_on_props_with_brightness,
): ):
"""Test lights with RGBW/RGBWW.""" """Test lights with RGBW/RGBWW with color temp support."""
entity_id = "light.demo" entity_id = "light.demo"
hass.states.async_set( hass.states.async_set(
@ -629,7 +633,7 @@ async def test_light_rgb_with_white(
await hass.async_block_till_done() await hass.async_block_till_done()
assert acc.char_hue.value == 23 assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100 assert acc.char_saturation.value == 100
assert acc.char_brightness.value == 50 assert acc.char_brightness.value == 100
# Set from HomeKit # Set from HomeKit
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
@ -658,11 +662,10 @@ async def test_light_rgb_with_white(
await _wait_for_light_coalesce(hass) await _wait_for_light_coalesce(hass)
assert call_turn_on assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
for k, v in turn_on_props.items(): assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75)
assert call_turn_on[-1].data[k] == v
assert len(events) == 1 assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)"
assert acc.char_brightness.value == 50 assert acc.char_brightness.value == 100
hk_driver.set_characteristics( hk_driver.set_characteristics(
{ {
@ -697,7 +700,204 @@ async def test_light_rgb_with_white(
@pytest.mark.parametrize( @pytest.mark.parametrize(
"supported_color_modes, state_props, turn_on_props, turn_on_props_with_brightness", "supported_color_modes, state_props, turn_on_props_with_brightness",
[
[
[COLOR_MODE_RGBW],
{
ATTR_RGBW_COLOR: (128, 50, 0, 255),
ATTR_RGB_COLOR: (128, 50, 0),
ATTR_HS_COLOR: (23.438, 100.0),
ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBW,
},
{ATTR_RGBW_COLOR: (0, 0, 0, 191)},
],
[
[COLOR_MODE_RGBWW],
{
ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255),
ATTR_RGB_COLOR: (128, 50, 0),
ATTR_HS_COLOR: (23.438, 100.0),
ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBWW,
},
{ATTR_RGBWW_COLOR: (0, 0, 0, 165, 26)},
],
],
)
async def test_light_rgbwx_with_color_temp_and_brightness(
hass,
hk_driver,
events,
supported_color_modes,
state_props,
turn_on_props_with_brightness,
):
"""Test lights with RGBW/RGBWW with color temp support and setting brightness."""
entity_id = "light.demo"
hass.states.async_set(
entity_id,
STATE_ON,
{ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, **state_props},
)
await hass.async_block_till_done()
acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
hk_driver.add_accessory(acc)
assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100
await acc.run()
await hass.async_block_till_done()
assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100
assert acc.char_brightness.value == 100
# Set from HomeKit
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID]
char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID]
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_color_temp_iid,
HAP_REPR_VALUE: 200,
},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_brightness_iid,
HAP_REPR_VALUE: 75,
},
]
},
"mock_addr",
)
await _wait_for_light_coalesce(hass)
assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
for k, v in turn_on_props_with_brightness.items():
assert call_turn_on[-1].data[k] == v
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "brightness at 75%, color temperature at 200"
assert acc.char_brightness.value == 75
async def test_light_rgb_or_w_lights(
hass,
hk_driver,
events,
):
"""Test lights with RGB or W lights."""
entity_id = "light.demo"
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGB, COLOR_MODE_WHITE],
ATTR_RGBW_COLOR: (128, 50, 0, 255),
ATTR_RGB_COLOR: (128, 50, 0),
ATTR_HS_COLOR: (23.438, 100.0),
ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGB,
},
)
await hass.async_block_till_done()
acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
hk_driver.add_accessory(acc)
assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100
await acc.run()
await hass.async_block_till_done()
assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100
assert acc.char_brightness.value == 100
assert acc.char_color_temp.value == 153
# Set from HomeKit
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID]
char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID]
char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID]
char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID]
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_hue_iid,
HAP_REPR_VALUE: 145,
},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_saturation_iid,
HAP_REPR_VALUE: 75,
},
]
},
"mock_addr",
)
await _wait_for_light_coalesce(hass)
assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75)
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)"
assert acc.char_brightness.value == 100
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_color_temp_iid,
HAP_REPR_VALUE: acc.min_mireds,
},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_brightness_iid,
HAP_REPR_VALUE: 25,
},
]
},
"mock_addr",
)
await _wait_for_light_coalesce(hass)
assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[-1].data[ATTR_WHITE] == round(25 * 255 / 100)
assert len(events) == 2
assert events[-1].data[ATTR_VALUE] == "brightness at 25%, color temperature at 153"
assert acc.char_brightness.value == 25
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGB, COLOR_MODE_WHITE],
ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_WHITE,
},
)
await hass.async_block_till_done()
assert acc.char_hue.value == 0
assert acc.char_saturation.value == 0
assert acc.char_brightness.value == 100
assert acc.char_color_temp.value == 153
@pytest.mark.parametrize(
"supported_color_modes, state_props",
[ [
[ [
[COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBW], [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBW],
@ -708,8 +908,6 @@ async def test_light_rgb_with_white(
ATTR_BRIGHTNESS: 255, ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBW, ATTR_COLOR_MODE: COLOR_MODE_RGBW,
}, },
{ATTR_RGBW_COLOR: (31, 127, 71, 0)},
{ATTR_COLOR_TEMP: 2700},
], ],
[ [
[COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBWW], [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBWW],
@ -720,8 +918,6 @@ async def test_light_rgb_with_white(
ATTR_BRIGHTNESS: 255, ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBWW, ATTR_COLOR_MODE: COLOR_MODE_RGBWW,
}, },
{ATTR_RGBWW_COLOR: (31, 127, 71, 0, 0)},
{ATTR_COLOR_TEMP: 2700},
], ],
], ],
) )
@ -731,8 +927,6 @@ async def test_light_rgb_with_white_switch_to_temp(
events, events,
supported_color_modes, supported_color_modes,
state_props, state_props,
turn_on_props,
turn_on_props_with_brightness,
): ):
"""Test lights with RGBW/RGBWW that preserves brightness when switching to color temp.""" """Test lights with RGBW/RGBWW that preserves brightness when switching to color temp."""
entity_id = "light.demo" entity_id = "light.demo"
@ -753,7 +947,7 @@ async def test_light_rgb_with_white_switch_to_temp(
await hass.async_block_till_done() await hass.async_block_till_done()
assert acc.char_hue.value == 23 assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100 assert acc.char_saturation.value == 100
assert acc.char_brightness.value == 50 assert acc.char_brightness.value == 100
# Set from HomeKit # Set from HomeKit
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
@ -782,19 +976,17 @@ async def test_light_rgb_with_white_switch_to_temp(
await _wait_for_light_coalesce(hass) await _wait_for_light_coalesce(hass)
assert call_turn_on assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
for k, v in turn_on_props.items(): assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75)
assert call_turn_on[-1].data[k] == v
assert len(events) == 1 assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)"
assert acc.char_brightness.value == 50 assert acc.char_brightness.value == 100
hk_driver.set_characteristics( hk_driver.set_characteristics(
{ {
HAP_REPR_CHARS: [ HAP_REPR_CHARS: [
{ {
HAP_REPR_AID: acc.aid, HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_color_temp_iid, HAP_REPR_IID: char_color_temp_iid,
HAP_REPR_VALUE: 2700, HAP_REPR_VALUE: 500,
}, },
] ]
}, },
@ -803,11 +995,221 @@ async def test_light_rgb_with_white_switch_to_temp(
await _wait_for_light_coalesce(hass) await _wait_for_light_coalesce(hass)
assert call_turn_on assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
for k, v in turn_on_props_with_brightness.items(): assert call_turn_on[-1].data[ATTR_COLOR_TEMP] == 500
assert call_turn_on[-1].data[k] == v
assert len(events) == 2 assert len(events) == 2
assert events[-1].data[ATTR_VALUE] == "color temperature at 2700" assert events[-1].data[ATTR_VALUE] == "color temperature at 500"
assert acc.char_brightness.value == 50 assert acc.char_brightness.value == 100
async def test_light_rgbww_with_color_temp_conversion(
hass,
hk_driver,
events,
):
"""Test lights with RGBWW convert color temp as expected."""
entity_id = "light.demo"
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBWW],
ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255),
ATTR_RGB_COLOR: (128, 50, 0),
ATTR_HS_COLOR: (23.438, 100.0),
ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBWW,
},
)
await hass.async_block_till_done()
acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
hk_driver.add_accessory(acc)
assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100
await acc.run()
await hass.async_block_till_done()
assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100
assert acc.char_brightness.value == 100
# Set from HomeKit
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID]
char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID]
char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID]
char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID]
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_hue_iid,
HAP_REPR_VALUE: 145,
},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_saturation_iid,
HAP_REPR_VALUE: 75,
},
]
},
"mock_addr",
)
await _wait_for_light_coalesce(hass)
assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75)
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)"
assert acc.char_brightness.value == 100
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_color_temp_iid,
HAP_REPR_VALUE: 200,
},
]
},
"mock_addr",
)
await _wait_for_light_coalesce(hass)
assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[-1].data[ATTR_RGBWW_COLOR] == (0, 0, 0, 220, 35)
assert len(events) == 2
assert events[-1].data[ATTR_VALUE] == "color temperature at 200"
assert acc.char_brightness.value == 100
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBWW],
ATTR_RGBWW_COLOR: (0, 0, 0, 128, 255),
ATTR_RGB_COLOR: (255, 163, 79),
ATTR_HS_COLOR: (28.636, 69.02),
ATTR_BRIGHTNESS: 180,
ATTR_COLOR_MODE: COLOR_MODE_RGBWW,
},
)
await hass.async_block_till_done()
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_brightness_iid,
HAP_REPR_VALUE: 100,
},
]
},
"mock_addr",
)
await _wait_for_light_coalesce(hass)
assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[-1].data[ATTR_BRIGHTNESS_PCT] == 100
assert len(events) == 3
assert events[-1].data[ATTR_VALUE] == "brightness at 100%"
assert acc.char_brightness.value == 100
async def test_light_rgbw_with_color_temp_conversion(
hass,
hk_driver,
events,
):
"""Test lights with RGBW convert color temp as expected."""
entity_id = "light.demo"
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW],
ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255),
ATTR_RGB_COLOR: (128, 50, 0),
ATTR_HS_COLOR: (23.438, 100.0),
ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBW,
},
)
await hass.async_block_till_done()
acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
hk_driver.add_accessory(acc)
assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100
await acc.run()
await hass.async_block_till_done()
assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100
assert acc.char_brightness.value == 100
# Set from HomeKit
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID]
char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID]
char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID]
assert (
acc.char_color_temp.properties[PROP_MIN_VALUE]
== acc.char_color_temp.properties[PROP_MAX_VALUE]
)
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_hue_iid,
HAP_REPR_VALUE: 145,
},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_saturation_iid,
HAP_REPR_VALUE: 75,
},
]
},
"mock_addr",
)
await _wait_for_light_coalesce(hass)
assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75)
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)"
assert acc.char_brightness.value == 100
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_color_temp_iid,
HAP_REPR_VALUE: 153,
},
]
},
"mock_addr",
)
await _wait_for_light_coalesce(hass)
assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[-1].data[ATTR_RGBW_COLOR] == (0, 0, 0, 255)
assert len(events) == 2
assert events[-1].data[ATTR_VALUE] == "color temperature at 153"
assert acc.char_brightness.value == 100
async def test_light_set_brightness_and_color(hass, hk_driver, events): async def test_light_set_brightness_and_color(hass, hk_driver, events):