Fix HomeKit reporting/setting colors when white values are present (#63948)

This commit is contained in:
J. Nick Koston 2022-01-12 12:58:25 -10:00 committed by GitHub
parent 1019156899
commit 5622db10b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 298 additions and 7 deletions

View file

@ -7,11 +7,17 @@ from pyhap.const import CATEGORY_LIGHTBULB
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT,
ATTR_COLOR_MODE,
ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
ATTR_MAX_MIREDS,
ATTR_MIN_MIREDS,
ATTR_RGB_COLOR,
ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR,
ATTR_SUPPORTED_COLOR_MODES,
COLOR_MODE_RGBW,
COLOR_MODE_RGBWW,
DOMAIN,
brightness_supported,
color_supported,
@ -26,6 +32,7 @@ from homeassistant.const import (
from homeassistant.core import callback
from homeassistant.helpers.event import async_call_later
from homeassistant.util.color import (
color_hsv_to_RGB,
color_temperature_mired_to_kelvin,
color_temperature_to_hs,
)
@ -49,6 +56,9 @@ RGB_COLOR = "rgb_color"
CHANGE_COALESCE_TIME_WINDOW = 0.01
COLOR_MODES_WITH_WHITES = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW}
@TYPES.register("Light")
class Light(HomeAccessory):
"""Generate a Light accessory for a light entity.
@ -66,7 +76,9 @@ class Light(HomeAccessory):
state = self.hass.states.get(self.entity_id)
attributes = state.attributes
color_modes = attributes.get(ATTR_SUPPORTED_COLOR_MODES)
self.color_modes = color_modes = (
attributes.get(ATTR_SUPPORTED_COLOR_MODES) or []
)
self.color_supported = color_supported(color_modes)
self.color_temp_supported = color_temp_supported(color_modes)
self.brightness_supported = brightness_supported(color_modes)
@ -138,12 +150,13 @@ class Light(HomeAccessory):
service = SERVICE_TURN_OFF
events.append(f"Set state to {char_values[CHAR_ON]}")
brightness_pct = None
if CHAR_BRIGHTNESS in char_values:
if char_values[CHAR_BRIGHTNESS] == 0:
events[-1] = "Set state to 0"
service = SERVICE_TURN_OFF
else:
params[ATTR_BRIGHTNESS_PCT] = char_values[CHAR_BRIGHTNESS]
brightness_pct = char_values[CHAR_BRIGHTNESS]
events.append(f"brightness at {char_values[CHAR_BRIGHTNESS]}%")
if service == SERVICE_TURN_OFF:
@ -156,13 +169,36 @@ class Light(HomeAccessory):
params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE]
events.append(f"color temperature at {params[ATTR_COLOR_TEMP]}")
elif CHAR_HUE in char_values or CHAR_SATURATION in char_values:
color = params[ATTR_HS_COLOR] = (
elif (
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 = (
char_values.get(CHAR_HUE, self.char_hue.value),
char_values.get(CHAR_SATURATION, self.char_saturation.value),
)
_LOGGER.debug("%s: Set hs_color to %s", self.entity_id, color)
events.append(f"set color at {color}")
_LOGGER.debug("%s: Set hs_color to %s", self.entity_id, hue_sat)
events.append(f"set color at {hue_sat}")
# HomeKit doesn't support RGBW/RGBWW so we need to remove any white values
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 (
brightness_pct
and ATTR_RGBWW_COLOR not in params
and ATTR_RGBW_COLOR not in params
):
params[ATTR_BRIGHTNESS_PCT] = brightness_pct
self.async_call_service(DOMAIN, service, params, ", ".join(events))
@ -172,11 +208,21 @@ class Light(HomeAccessory):
# Handle State
state = new_state.state
attributes = new_state.attributes
color_mode = attributes.get(ATTR_COLOR_MODE)
self.char_on.set_value(int(state == STATE_ON))
# Handle Brightness
if self.brightness_supported:
brightness = attributes.get(ATTR_BRIGHTNESS)
if (
color_mode
and COLOR_MODES_WITH_WHITES.intersection({color_mode})
and (rgb_color := attributes.get(ATTR_RGB_COLOR))
):
# HomeKit doesn't support RGBW/RGBWW so we need to
# give it the color brightness only
brightness = max(rgb_color)
else:
brightness = attributes.get(ATTR_BRIGHTNESS)
if isinstance(brightness, (int, float)):
brightness = round(brightness / 255 * 100, 0)
# The homeassistant component might report its brightness as 0 but is

View file

@ -13,11 +13,18 @@ from homeassistant.components.homekit.type_lights import (
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT,
ATTR_COLOR_MODE,
ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
ATTR_MAX_MIREDS,
ATTR_MIN_MIREDS,
ATTR_RGB_COLOR,
ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR,
ATTR_SUPPORTED_COLOR_MODES,
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_RGBW,
COLOR_MODE_RGBWW,
DOMAIN,
)
from homeassistant.const import (
@ -565,6 +572,244 @@ async def test_light_restore(hass, hk_driver, events):
assert acc.char_on.value == 0
@pytest.mark.parametrize(
"supported_color_modes, state_props, turn_on_props, turn_on_props_with_brightness",
[
[
[COLOR_MODE_COLOR_TEMP, 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: (31, 127, 71, 0)},
{ATTR_RGBW_COLOR: (15, 63, 35, 0)},
],
[
[COLOR_MODE_COLOR_TEMP, 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: (31, 127, 71, 0, 0)},
{ATTR_RGBWW_COLOR: (15, 63, 35, 0, 0)},
],
],
)
async def test_light_rgb_with_white(
hass,
hk_driver,
events,
supported_color_modes,
state_props,
turn_on_props,
turn_on_props_with_brightness,
):
"""Test lights with RGBW/RGBWW."""
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 == 50
# 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]
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
for k, v in turn_on_props.items():
assert call_turn_on[-1].data[k] == v
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)"
assert acc.char_brightness.value == 50
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,
},
{
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
for k, v in turn_on_props_with_brightness.items():
assert call_turn_on[-1].data[k] == v
assert len(events) == 2
assert events[-1].data[ATTR_VALUE] == "brightness at 25%, set color at (145, 75)"
assert acc.char_brightness.value == 25
@pytest.mark.parametrize(
"supported_color_modes, state_props, turn_on_props, turn_on_props_with_brightness",
[
[
[COLOR_MODE_COLOR_TEMP, 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: (31, 127, 71, 0)},
{ATTR_COLOR_TEMP: 2700},
],
[
[COLOR_MODE_COLOR_TEMP, 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: (31, 127, 71, 0, 0)},
{ATTR_COLOR_TEMP: 2700},
],
],
)
async def test_light_rgb_with_white_switch_to_temp(
hass,
hk_driver,
events,
supported_color_modes,
state_props,
turn_on_props,
turn_on_props_with_brightness,
):
"""Test lights with RGBW/RGBWW that preserves brightness when switching to color temp."""
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 == 50
# 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]
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
for k, v in turn_on_props.items():
assert call_turn_on[-1].data[k] == v
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)"
assert acc.char_brightness.value == 50
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_color_temp_iid,
HAP_REPR_VALUE: 2700,
},
]
},
"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) == 2
assert events[-1].data[ATTR_VALUE] == "color temperature at 2700"
assert acc.char_brightness.value == 50
async def test_light_set_brightness_and_color(hass, hk_driver, events):
"""Test light with all chars in one go."""
entity_id = "light.demo"