mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 20:27:08 +00:00
Fix HomeKit reporting/setting colors when white values are present (#63948)
This commit is contained in:
parent
1019156899
commit
5622db10b1
@ -7,11 +7,17 @@ from pyhap.const import CATEGORY_LIGHTBULB
|
|||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
ATTR_BRIGHTNESS_PCT,
|
ATTR_BRIGHTNESS_PCT,
|
||||||
|
ATTR_COLOR_MODE,
|
||||||
ATTR_COLOR_TEMP,
|
ATTR_COLOR_TEMP,
|
||||||
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_RGBWW_COLOR,
|
||||||
ATTR_SUPPORTED_COLOR_MODES,
|
ATTR_SUPPORTED_COLOR_MODES,
|
||||||
|
COLOR_MODE_RGBW,
|
||||||
|
COLOR_MODE_RGBWW,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
brightness_supported,
|
brightness_supported,
|
||||||
color_supported,
|
color_supported,
|
||||||
@ -26,6 +32,7 @@ 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,
|
||||||
)
|
)
|
||||||
@ -49,6 +56,9 @@ RGB_COLOR = "rgb_color"
|
|||||||
CHANGE_COALESCE_TIME_WINDOW = 0.01
|
CHANGE_COALESCE_TIME_WINDOW = 0.01
|
||||||
|
|
||||||
|
|
||||||
|
COLOR_MODES_WITH_WHITES = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW}
|
||||||
|
|
||||||
|
|
||||||
@TYPES.register("Light")
|
@TYPES.register("Light")
|
||||||
class Light(HomeAccessory):
|
class Light(HomeAccessory):
|
||||||
"""Generate a Light accessory for a light entity.
|
"""Generate a Light accessory for a light entity.
|
||||||
@ -66,7 +76,9 @@ class Light(HomeAccessory):
|
|||||||
|
|
||||||
state = self.hass.states.get(self.entity_id)
|
state = self.hass.states.get(self.entity_id)
|
||||||
attributes = state.attributes
|
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_supported = color_supported(color_modes)
|
||||||
self.color_temp_supported = color_temp_supported(color_modes)
|
self.color_temp_supported = color_temp_supported(color_modes)
|
||||||
self.brightness_supported = brightness_supported(color_modes)
|
self.brightness_supported = brightness_supported(color_modes)
|
||||||
@ -138,12 +150,13 @@ class Light(HomeAccessory):
|
|||||||
service = SERVICE_TURN_OFF
|
service = SERVICE_TURN_OFF
|
||||||
events.append(f"Set state to {char_values[CHAR_ON]}")
|
events.append(f"Set state to {char_values[CHAR_ON]}")
|
||||||
|
|
||||||
|
brightness_pct = None
|
||||||
if CHAR_BRIGHTNESS in char_values:
|
if CHAR_BRIGHTNESS in char_values:
|
||||||
if char_values[CHAR_BRIGHTNESS] == 0:
|
if char_values[CHAR_BRIGHTNESS] == 0:
|
||||||
events[-1] = "Set state to 0"
|
events[-1] = "Set state to 0"
|
||||||
service = SERVICE_TURN_OFF
|
service = SERVICE_TURN_OFF
|
||||||
else:
|
else:
|
||||||
params[ATTR_BRIGHTNESS_PCT] = char_values[CHAR_BRIGHTNESS]
|
brightness_pct = char_values[CHAR_BRIGHTNESS]
|
||||||
events.append(f"brightness at {char_values[CHAR_BRIGHTNESS]}%")
|
events.append(f"brightness at {char_values[CHAR_BRIGHTNESS]}%")
|
||||||
|
|
||||||
if service == SERVICE_TURN_OFF:
|
if service == SERVICE_TURN_OFF:
|
||||||
@ -156,13 +169,36 @@ class Light(HomeAccessory):
|
|||||||
params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE]
|
params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE]
|
||||||
events.append(f"color temperature at {params[ATTR_COLOR_TEMP]}")
|
events.append(f"color temperature at {params[ATTR_COLOR_TEMP]}")
|
||||||
|
|
||||||
elif CHAR_HUE in char_values or CHAR_SATURATION in char_values:
|
elif (
|
||||||
color = params[ATTR_HS_COLOR] = (
|
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_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, color)
|
_LOGGER.debug("%s: Set hs_color to %s", self.entity_id, hue_sat)
|
||||||
events.append(f"set color at {color}")
|
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))
|
self.async_call_service(DOMAIN, service, params, ", ".join(events))
|
||||||
|
|
||||||
@ -172,11 +208,21 @@ class Light(HomeAccessory):
|
|||||||
# Handle State
|
# Handle State
|
||||||
state = new_state.state
|
state = new_state.state
|
||||||
attributes = new_state.attributes
|
attributes = new_state.attributes
|
||||||
|
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))
|
||||||
|
|
||||||
# Handle Brightness
|
# Handle Brightness
|
||||||
if self.brightness_supported:
|
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)):
|
if isinstance(brightness, (int, float)):
|
||||||
brightness = round(brightness / 255 * 100, 0)
|
brightness = round(brightness / 255 * 100, 0)
|
||||||
# The homeassistant component might report its brightness as 0 but is
|
# 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 (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
ATTR_BRIGHTNESS_PCT,
|
ATTR_BRIGHTNESS_PCT,
|
||||||
|
ATTR_COLOR_MODE,
|
||||||
ATTR_COLOR_TEMP,
|
ATTR_COLOR_TEMP,
|
||||||
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_RGBWW_COLOR,
|
||||||
ATTR_SUPPORTED_COLOR_MODES,
|
ATTR_SUPPORTED_COLOR_MODES,
|
||||||
|
COLOR_MODE_COLOR_TEMP,
|
||||||
|
COLOR_MODE_RGBW,
|
||||||
|
COLOR_MODE_RGBWW,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -565,6 +572,244 @@ async def test_light_restore(hass, hk_driver, events):
|
|||||||
assert acc.char_on.value == 0
|
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):
|
async def test_light_set_brightness_and_color(hass, hk_driver, events):
|
||||||
"""Test light with all chars in one go."""
|
"""Test light with all chars in one go."""
|
||||||
entity_id = "light.demo"
|
entity_id = "light.demo"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user