Use homekit service callbacks for lights to resolve out of sync states (#32348)

* Switch homekit lights to use service callbacks

Service callbacks allow us to get the on/off, brightness, etc
all in one call so we remove all the complexity that was
previously needed to handle the out of sync states

We now get the on event and brightness event at the same time
which allows us to prevent lights from flashing up to 100%
before the requested brightness.

* Fix STATE_OFF -> STATE_ON,brightness:0
pull/31889/head^2
J. Nick Koston 2020-04-02 20:06:13 -05:00 committed by GitHub
parent 2b0bdd580c
commit f25321e010
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 343 additions and 128 deletions

View File

@ -25,7 +25,7 @@ from homeassistant.const import (
)
from . import TYPES
from .accessories import HomeAccessory, debounce
from .accessories import HomeAccessory
from .const import (
CHAR_BRIGHTNESS,
CHAR_COLOR_TEMPERATURE,
@ -52,15 +52,6 @@ class Light(HomeAccessory):
def __init__(self, *args):
"""Initialize a new Light accessory object."""
super().__init__(*args, category=CATEGORY_LIGHTBULB)
self._flag = {
CHAR_ON: False,
CHAR_BRIGHTNESS: False,
CHAR_HUE: False,
CHAR_SATURATION: False,
CHAR_COLOR_TEMPERATURE: False,
RGB_COLOR: False,
}
self._state = 0
self.chars = []
self._features = self.hass.states.get(self.entity_id).attributes.get(
@ -82,17 +73,14 @@ class Light(HomeAccessory):
self.chars.append(CHAR_COLOR_TEMPERATURE)
serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars)
self.char_on = serv_light.configure_char(
CHAR_ON, value=self._state, setter_callback=self.set_state
)
self.char_on = serv_light.configure_char(CHAR_ON, value=0)
if CHAR_BRIGHTNESS in self.chars:
# Initial value is set to 100 because 0 is a special value (off). 100 is
# an arbitrary non-zero value. It is updated immediately by update_state
# to set to the correct initial value.
self.char_brightness = serv_light.configure_char(
CHAR_BRIGHTNESS, value=100, setter_callback=self.set_brightness
)
self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100)
if CHAR_COLOR_TEMPERATURE in self.chars:
min_mireds = self.hass.states.get(self.entity_id).attributes.get(
@ -105,133 +93,98 @@ class Light(HomeAccessory):
CHAR_COLOR_TEMPERATURE,
value=min_mireds,
properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds},
setter_callback=self.set_color_temperature,
)
if CHAR_HUE in self.chars:
self.char_hue = serv_light.configure_char(
CHAR_HUE, value=0, setter_callback=self.set_hue
)
self.char_hue = serv_light.configure_char(CHAR_HUE, value=0)
if CHAR_SATURATION in self.chars:
self.char_saturation = serv_light.configure_char(
CHAR_SATURATION, value=75, setter_callback=self.set_saturation
)
self.char_saturation = serv_light.configure_char(CHAR_SATURATION, value=75)
def set_state(self, value):
"""Set state if call came from HomeKit."""
if self._state == value:
return
serv_light.setter_callback = self._set_chars
_LOGGER.debug("%s: Set state to %d", self.entity_id, value)
self._flag[CHAR_ON] = True
def _set_chars(self, char_values):
_LOGGER.debug("_set_chars: %s", char_values)
events = []
service = SERVICE_TURN_ON
params = {ATTR_ENTITY_ID: self.entity_id}
service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF
self.call_service(DOMAIN, service, params)
if CHAR_ON in char_values:
if not char_values[CHAR_ON]:
service = SERVICE_TURN_OFF
events.append(f"Set state to {char_values[CHAR_ON]}")
@debounce
def set_brightness(self, value):
"""Set brightness if call came from HomeKit."""
_LOGGER.debug("%s: Set brightness to %d", self.entity_id, value)
self._flag[CHAR_BRIGHTNESS] = True
if value == 0:
self.set_state(0) # Turn off light
return
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_BRIGHTNESS_PCT: value}
self.call_service(DOMAIN, SERVICE_TURN_ON, params, f"brightness at {value}%")
if CHAR_BRIGHTNESS in char_values:
if char_values[CHAR_BRIGHTNESS] == 0:
events[-1] = f"Set state to 0"
service = SERVICE_TURN_OFF
else:
params[ATTR_BRIGHTNESS_PCT] = char_values[CHAR_BRIGHTNESS]
events.append(f"brightness at {char_values[CHAR_BRIGHTNESS]}%")
def set_color_temperature(self, value):
"""Set color temperature if call came from HomeKit."""
_LOGGER.debug("%s: Set color temp to %s", self.entity_id, value)
self._flag[CHAR_COLOR_TEMPERATURE] = True
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_COLOR_TEMP: value}
self.call_service(
DOMAIN, SERVICE_TURN_ON, params, f"color temperature at {value}"
)
if CHAR_COLOR_TEMPERATURE in char_values:
params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE]
events.append(f"color temperature at {char_values[CHAR_COLOR_TEMPERATURE]}")
def set_saturation(self, value):
"""Set saturation if call came from HomeKit."""
_LOGGER.debug("%s: Set saturation to %d", self.entity_id, value)
self._flag[CHAR_SATURATION] = True
self._saturation = value
self.set_color()
def set_hue(self, value):
"""Set hue if call came from HomeKit."""
_LOGGER.debug("%s: Set hue to %d", self.entity_id, value)
self._flag[CHAR_HUE] = True
self._hue = value
self.set_color()
def set_color(self):
"""Set color if call came from HomeKit."""
if (
self._features & SUPPORT_COLOR
and self._flag[CHAR_HUE]
and self._flag[CHAR_SATURATION]
and CHAR_HUE in char_values
and CHAR_SATURATION in char_values
):
color = (self._hue, self._saturation)
color = (char_values[CHAR_HUE], char_values[CHAR_SATURATION])
_LOGGER.debug("%s: Set hs_color to %s", self.entity_id, color)
self._flag.update(
{CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}
)
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HS_COLOR: color}
self.call_service(DOMAIN, SERVICE_TURN_ON, params, f"set color at {color}")
params[ATTR_HS_COLOR] = color
events.append(f"set color at {color}")
self.call_service(DOMAIN, service, params, ", ".join(events))
def update_state(self, new_state):
"""Update light after state change."""
# Handle State
state = new_state.state
if state in (STATE_ON, STATE_OFF):
self._state = 1 if state == STATE_ON else 0
if not self._flag[CHAR_ON] and self.char_on.value != self._state:
self.char_on.set_value(self._state)
self._flag[CHAR_ON] = False
if state == STATE_ON and self.char_on.value != 1:
self.char_on.set_value(1)
elif state == STATE_OFF and self.char_on.value != 0:
self.char_on.set_value(0)
# Handle Brightness
if CHAR_BRIGHTNESS in self.chars:
brightness = new_state.attributes.get(ATTR_BRIGHTNESS)
if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int):
if isinstance(brightness, int):
brightness = round(brightness / 255 * 100, 0)
# The homeassistant component might report its brightness as 0 but is
# not off. But 0 is a special value in homekit. When you turn on a
# homekit accessory it will try to restore the last brightness state
# which will be the last value saved by char_brightness.set_value.
# But if it is set to 0, HomeKit will update the brightness to 100 as
# it thinks 0 is off.
#
# Therefore, if the the brightness is 0 and the device is still on,
# 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
if self.char_brightness.value != brightness:
# The homeassistant component might report its brightness as 0 but is
# not off. But 0 is a special value in homekit. When you turn on a
# homekit accessory it will try to restore the last brightness state
# which will be the last value saved by char_brightness.set_value.
# But if it is set to 0, HomeKit will update the brightness to 100 as
# it thinks 0 is off.
#
# Therefore, if the the brightness is 0 and the device is still on,
# the brightness is mapped to 1 otherwise the update is ignored in
# order to avoid this incorrect behavior.
if brightness == 0:
if state == STATE_ON:
self.char_brightness.set_value(1)
else:
self.char_brightness.set_value(brightness)
self._flag[CHAR_BRIGHTNESS] = False
self.char_brightness.set_value(brightness)
# Handle color temperature
if CHAR_COLOR_TEMPERATURE in self.chars:
color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP)
if (
not self._flag[CHAR_COLOR_TEMPERATURE]
and isinstance(color_temperature, int)
isinstance(color_temperature, int)
and self.char_color_temperature.value != color_temperature
):
self.char_color_temperature.set_value(color_temperature)
self._flag[CHAR_COLOR_TEMPERATURE] = False
# Handle Color
if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars:
hue, saturation = new_state.attributes.get(ATTR_HS_COLOR, (None, None))
if (
not self._flag[RGB_COLOR]
and (hue != self._hue or saturation != self._saturation)
and isinstance(hue, (int, float))
isinstance(hue, (int, float))
and isinstance(saturation, (int, float))
and (
hue != self.char_hue.value
or saturation != self.char_saturation.value
)
):
self.char_hue.set_value(hue)
self.char_saturation.set_value(saturation)
self._hue, self._saturation = (hue, saturation)
self._flag[RGB_COLOR] = False

View File

@ -1,6 +1,9 @@
"""Test different accessory types: Lights."""
from collections import namedtuple
from asynctest import patch
from pyhap.accessory_driver import AccessoryDriver
from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE
import pytest
from homeassistant.components.homekit.const import ATTR_VALUE
@ -30,6 +33,15 @@ from tests.common import async_mock_service
from tests.components.homekit.common import patch_debounce
@pytest.fixture
def driver():
"""Patch AccessoryDriver without zeroconf or HAPServer."""
with patch("pyhap.accessory_driver.HAPServer"), patch(
"pyhap.accessory_driver.Zeroconf"
), patch("pyhap.accessory_driver.AccessoryDriver.persist"):
yield AccessoryDriver()
@pytest.fixture(scope="module")
def cls():
"""Patch debounce decorator during import of type_lights."""
@ -43,15 +55,16 @@ def cls():
patcher.stop()
async def test_light_basic(hass, hk_driver, cls, events):
async def test_light_basic(hass, hk_driver, cls, events, driver):
"""Test light with char state."""
entity_id = "light.demo"
hass.states.async_set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0})
await hass.async_block_till_done()
acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None)
acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
driver.add_accessory(acc)
assert acc.aid == 2
assert acc.aid == 1
assert acc.category == 5 # Lightbulb
assert acc.char_on.value == 0
@ -75,25 +88,43 @@ async def test_light_basic(hass, hk_driver, cls, events):
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
call_turn_off = async_mock_service(hass, DOMAIN, "turn_off")
char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID]
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}
]
},
"mock_addr",
)
await hass.async_add_job(acc.char_on.client_update_value, 1)
await hass.async_block_till_done()
assert call_turn_on
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] is None
assert events[-1].data[ATTR_VALUE] == "Set state to 1"
hass.states.async_set(entity_id, STATE_ON)
await hass.async_block_till_done()
await hass.async_add_job(acc.char_on.client_update_value, 0)
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 0}
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert call_turn_off
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
assert len(events) == 2
assert events[-1].data[ATTR_VALUE] is None
assert events[-1].data[ATTR_VALUE] == "Set state to 0"
async def test_light_brightness(hass, hk_driver, cls, events):
async def test_light_brightness(hass, hk_driver, cls, events, driver):
"""Test light with brightness."""
entity_id = "light.demo"
@ -103,11 +134,14 @@ async def test_light_brightness(hass, hk_driver, cls, events):
{ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255},
)
await hass.async_block_till_done()
acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None)
acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
driver.add_accessory(acc)
# Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the
# brightness to 100 when turning on a light on a freshly booted up server.
assert acc.char_brightness.value != 0
char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID]
char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID]
await hass.async_add_job(acc.run)
await hass.async_block_till_done()
@ -121,34 +155,88 @@ async def test_light_brightness(hass, hk_driver, cls, events):
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
call_turn_off = async_mock_service(hass, DOMAIN, "turn_off")
await hass.async_add_job(acc.char_brightness.client_update_value, 20)
await hass.async_add_job(acc.char_on.client_update_value, 1)
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_brightness_iid,
HAP_REPR_VALUE: 20,
},
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert call_turn_on[0]
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == f"brightness at 20{UNIT_PERCENTAGE}"
assert (
events[-1].data[ATTR_VALUE]
== f"Set state to 1, brightness at 20{UNIT_PERCENTAGE}"
)
await hass.async_add_job(acc.char_on.client_update_value, 1)
await hass.async_add_job(acc.char_brightness.client_update_value, 40)
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_brightness_iid,
HAP_REPR_VALUE: 40,
},
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert call_turn_on[1]
assert call_turn_on[1].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[1].data[ATTR_BRIGHTNESS_PCT] == 40
assert len(events) == 2
assert events[-1].data[ATTR_VALUE] == f"brightness at 40{UNIT_PERCENTAGE}"
assert (
events[-1].data[ATTR_VALUE]
== f"Set state to 1, brightness at 40{UNIT_PERCENTAGE}"
)
await hass.async_add_job(acc.char_on.client_update_value, 1)
await hass.async_add_job(acc.char_brightness.client_update_value, 0)
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_brightness_iid,
HAP_REPR_VALUE: 0,
},
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert call_turn_off
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
assert len(events) == 3
assert events[-1].data[ATTR_VALUE] is None
assert (
events[-1].data[ATTR_VALUE]
== f"Set state to 0, brightness at 0{UNIT_PERCENTAGE}"
)
# 0 is a special case for homekit, see "Handle Brightness"
# in update_state
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0})
await hass.async_block_till_done()
assert acc.char_brightness.value == 1
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 255})
await hass.async_block_till_done()
assert acc.char_brightness.value == 100
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0})
await hass.async_block_till_done()
assert acc.char_brightness.value == 1
async def test_light_color_temperature(hass, hk_driver, cls, events):
async def test_light_color_temperature(hass, hk_driver, cls, events, driver):
"""Test light with color temperature."""
entity_id = "light.demo"
@ -158,7 +246,8 @@ async def test_light_color_temperature(hass, hk_driver, cls, events):
{ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP: 190},
)
await hass.async_block_till_done()
acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None)
acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
driver.add_accessory(acc)
assert acc.char_color_temperature.value == 153
@ -169,6 +258,20 @@ async def test_light_color_temperature(hass, hk_driver, cls, events):
# Set from HomeKit
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID]
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_color_temperature_iid,
HAP_REPR_VALUE: 250,
}
]
},
"mock_addr",
)
await hass.async_add_job(acc.char_color_temperature.client_update_value, 250)
await hass.async_block_till_done()
assert call_turn_on
@ -197,7 +300,7 @@ async def test_light_color_temperature_and_rgb_color(hass, hk_driver, cls, event
assert not hasattr(acc, "char_color_temperature")
async def test_light_rgb_color(hass, hk_driver, cls, events):
async def test_light_rgb_color(hass, hk_driver, cls, events, driver):
"""Test light with rgb_color."""
entity_id = "light.demo"
@ -207,7 +310,8 @@ async def test_light_rgb_color(hass, hk_driver, cls, events):
{ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, ATTR_HS_COLOR: (260, 90)},
)
await hass.async_block_till_done()
acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None)
acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
driver.add_accessory(acc)
assert acc.char_hue.value == 0
assert acc.char_saturation.value == 75
@ -220,8 +324,26 @@ async def test_light_rgb_color(hass, hk_driver, cls, events):
# Set from HomeKit
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
await hass.async_add_job(acc.char_hue.client_update_value, 145)
await hass.async_add_job(acc.char_saturation.client_update_value, 75)
char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID]
char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID]
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 hass.async_block_till_done()
assert call_turn_on
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
@ -230,7 +352,7 @@ async def test_light_rgb_color(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)"
async def test_light_restore(hass, hk_driver, cls, events):
async def test_light_restore(hass, hk_driver, cls, events, driver):
"""Test setting up an entity from state in the event registry."""
hass.state = CoreState.not_running
@ -250,7 +372,9 @@ async def test_light_restore(hass, hk_driver, cls, events):
hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {})
await hass.async_block_till_done()
acc = cls.light(hass, hk_driver, "Light", "light.simple", 2, None)
acc = cls.light(hass, hk_driver, "Light", "light.simple", 1, None)
driver.add_accessory(acc)
assert acc.category == 5 # Lightbulb
assert acc.chars == []
assert acc.char_on.value == 0
@ -259,3 +383,141 @@ async def test_light_restore(hass, hk_driver, cls, events):
assert acc.category == 5 # Lightbulb
assert acc.chars == ["Brightness"]
assert acc.char_on.value == 0
async def test_light_set_brightness_and_color(hass, hk_driver, cls, events, driver):
"""Test light with all chars in one go."""
entity_id = "light.demo"
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR,
ATTR_BRIGHTNESS: 255,
},
)
await hass.async_block_till_done()
acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
driver.add_accessory(acc)
# Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the
# brightness to 100 when turning on a light on a freshly booted up server.
assert acc.char_brightness.value != 0
char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID]
char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID]
char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID]
char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID]
await hass.async_add_job(acc.run)
await hass.async_block_till_done()
assert acc.char_brightness.value == 100
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102})
await hass.async_block_till_done()
assert acc.char_brightness.value == 40
# Set from HomeKit
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_brightness_iid,
HAP_REPR_VALUE: 20,
},
{
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 hass.async_block_till_done()
assert call_turn_on[0]
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20
assert call_turn_on[0].data[ATTR_HS_COLOR] == (145, 75)
assert len(events) == 1
assert (
events[-1].data[ATTR_VALUE]
== f"Set state to 1, brightness at 20{UNIT_PERCENTAGE}, set color at (145, 75)"
)
async def test_light_set_brightness_and_color_temp(
hass, hk_driver, cls, events, driver
):
"""Test light with all chars in one go."""
entity_id = "light.demo"
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP,
ATTR_BRIGHTNESS: 255,
},
)
await hass.async_block_till_done()
acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
driver.add_accessory(acc)
# Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the
# brightness to 100 when turning on a light on a freshly booted up server.
assert acc.char_brightness.value != 0
char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID]
char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID]
char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID]
await hass.async_add_job(acc.run)
await hass.async_block_till_done()
assert acc.char_brightness.value == 100
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102})
await hass.async_block_till_done()
assert acc.char_brightness.value == 40
# Set from HomeKit
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
driver.set_characteristics(
{
HAP_REPR_CHARS: [
{HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_brightness_iid,
HAP_REPR_VALUE: 20,
},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_color_temperature_iid,
HAP_REPR_VALUE: 250,
},
]
},
"mock_addr",
)
await hass.async_block_till_done()
assert call_turn_on[0]
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20
assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250
assert len(events) == 1
assert (
events[-1].data[ATTR_VALUE]
== f"Set state to 1, brightness at 20{UNIT_PERCENTAGE}, color temperature at 250"
)