Fix HomeKit reporting/setting colors when white values are present (#63948)
This commit is contained in:
parent
1019156899
commit
5622db10b1
2 changed files with 298 additions and 7 deletions
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue