diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 169130a194a..aea760534fd 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -6,13 +6,11 @@ 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_SUPPORTED_COLOR_MODES, - COLOR_MODE_COLOR_TEMP, DOMAIN, brightness_supported, color_supported, @@ -25,13 +23,17 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import callback +from homeassistant.helpers.event import async_call_later +from homeassistant.util.color import ( + color_temperature_mired_to_kelvin, + color_temperature_to_hs, +) from .accessories import TYPES, HomeAccessory from .const import ( CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, CHAR_HUE, - CHAR_NAME, CHAR_ON, CHAR_SATURATION, PROP_MAX_VALUE, @@ -43,6 +45,8 @@ _LOGGER = logging.getLogger(__name__) RGB_COLOR = "rgb_color" +CHANGE_COALESCE_TIME_WINDOW = 0.01 + @TYPES.register("Light") class Light(HomeAccessory): @@ -55,102 +59,78 @@ class Light(HomeAccessory): """Initialize a new Light accessory object.""" super().__init__(*args, category=CATEGORY_LIGHTBULB) - self.chars_primary = [] - self.chars_secondary = [] + self.chars = [] + self._event_timer = None + self._pending_events = {} state = self.hass.states.get(self.entity_id) attributes = state.attributes color_modes = attributes.get(ATTR_SUPPORTED_COLOR_MODES) - self.is_color_supported = color_supported(color_modes) - self.is_color_temp_supported = color_temp_supported(color_modes) - self.color_and_temp_supported = ( - self.is_color_supported and self.is_color_temp_supported - ) - self.is_brightness_supported = brightness_supported(color_modes) + self.color_supported = color_supported(color_modes) + self.color_temp_supported = color_temp_supported(color_modes) + self.brightness_supported = brightness_supported(color_modes) - if self.is_brightness_supported: - self.chars_primary.append(CHAR_BRIGHTNESS) + if self.brightness_supported: + self.chars.append(CHAR_BRIGHTNESS) - if self.is_color_supported: - self.chars_primary.append(CHAR_HUE) - self.chars_primary.append(CHAR_SATURATION) + if self.color_supported: + self.chars.extend([CHAR_HUE, CHAR_SATURATION]) - if self.is_color_temp_supported: - if self.color_and_temp_supported: - self.chars_primary.append(CHAR_NAME) - self.chars_secondary.append(CHAR_NAME) - self.chars_secondary.append(CHAR_COLOR_TEMPERATURE) - if self.is_brightness_supported: - self.chars_secondary.append(CHAR_BRIGHTNESS) - else: - self.chars_primary.append(CHAR_COLOR_TEMPERATURE) + if self.color_temp_supported: + self.chars.append(CHAR_COLOR_TEMPERATURE) - serv_light_primary = self.add_preload_service( - SERV_LIGHTBULB, self.chars_primary - ) - serv_light_secondary = None - self.char_on_primary = serv_light_primary.configure_char(CHAR_ON, value=0) + serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) + self.char_on = serv_light.configure_char(CHAR_ON, value=0) - if self.color_and_temp_supported: - serv_light_secondary = self.add_preload_service( - SERV_LIGHTBULB, self.chars_secondary - ) - serv_light_primary.add_linked_service(serv_light_secondary) - serv_light_primary.configure_char(CHAR_NAME, value="RGB") - self.char_on_secondary = serv_light_secondary.configure_char( - CHAR_ON, value=0 - ) - serv_light_secondary.configure_char(CHAR_NAME, value="Temperature") - - if self.is_brightness_supported: + if self.brightness_supported: # 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 async_update_state # to set to the correct initial value. - self.char_brightness_primary = serv_light_primary.configure_char( - CHAR_BRIGHTNESS, value=100 - ) - if self.chars_secondary: - self.char_brightness_secondary = serv_light_secondary.configure_char( - CHAR_BRIGHTNESS, value=100 - ) + self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100) - if self.is_color_temp_supported: + if self.color_temp_supported: min_mireds = attributes.get(ATTR_MIN_MIREDS, 153) max_mireds = attributes.get(ATTR_MAX_MIREDS, 500) - serv_light = serv_light_secondary or serv_light_primary - self.char_color_temperature = serv_light.configure_char( + self.char_color_temp = serv_light.configure_char( CHAR_COLOR_TEMPERATURE, value=min_mireds, properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds}, ) - if self.is_color_supported: - self.char_hue = serv_light_primary.configure_char(CHAR_HUE, value=0) - self.char_saturation = serv_light_primary.configure_char( - CHAR_SATURATION, value=75 - ) + if self.color_supported: + self.char_hue = serv_light.configure_char(CHAR_HUE, value=0) + self.char_saturation = serv_light.configure_char(CHAR_SATURATION, value=75) self.async_update_state(state) + serv_light.setter_callback = self._set_chars - if self.color_and_temp_supported: - serv_light_primary.setter_callback = self._set_chars_primary - serv_light_secondary.setter_callback = self._set_chars_secondary - else: - serv_light_primary.setter_callback = self._set_chars + def _set_chars(self, char_values): + _LOGGER.debug("Light _set_chars: %s", char_values) + # Newest change always wins + if CHAR_COLOR_TEMPERATURE in self._pending_events and ( + CHAR_SATURATION in char_values or CHAR_HUE in char_values + ): + del self._pending_events[CHAR_COLOR_TEMPERATURE] + for char in (CHAR_HUE, CHAR_SATURATION): + if char in self._pending_events and CHAR_COLOR_TEMPERATURE in char_values: + del self._pending_events[char] - def _set_chars_primary(self, char_values): - """Primary service is RGB or W if only color or color temp is supported.""" - self._set_chars(char_values, True) + self._pending_events.update(char_values) + if self._event_timer: + self._event_timer() + self._event_timer = async_call_later( + self.hass, CHANGE_COALESCE_TIME_WINDOW, self._send_events + ) - def _set_chars_secondary(self, char_values): - """Secondary service is W if both color or color temp are supported.""" - self._set_chars(char_values, False) - - def _set_chars(self, char_values, is_primary=None): - _LOGGER.debug("Light _set_chars: %s, is_primary: %s", char_values, is_primary) + def _send_events(self, *_): + """Process all changes at once.""" + _LOGGER.debug("Coalesced _set_chars: %s", self._pending_events) + char_values = self._pending_events + self._pending_events = {} events = [] service = SERVICE_TURN_ON params = {ATTR_ENTITY_ID: self.entity_id} + if CHAR_ON in char_values: if not char_values[CHAR_ON]: service = SERVICE_TURN_OFF @@ -170,24 +150,16 @@ class Light(HomeAccessory): ) return - if self.is_color_temp_supported and ( - is_primary is False or CHAR_COLOR_TEMPERATURE in char_values - ): - params[ATTR_COLOR_TEMP] = char_values.get( - CHAR_COLOR_TEMPERATURE, self.char_color_temperature.value - ) + if CHAR_COLOR_TEMPERATURE in char_values: + params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE] events.append(f"color temperature at {params[ATTR_COLOR_TEMP]}") - if self.is_color_supported and ( - is_primary is True - or (CHAR_HUE in char_values and CHAR_SATURATION in char_values) - ): - color = ( + elif CHAR_HUE in char_values or CHAR_SATURATION in char_values: + color = params[ATTR_HS_COLOR] = ( 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) - params[ATTR_HS_COLOR] = color events.append(f"set color at {color}") self.async_call_service(DOMAIN, service, params, ", ".join(events)) @@ -198,20 +170,10 @@ class Light(HomeAccessory): # Handle State state = new_state.state attributes = new_state.attributes - char_on_value = int(state == STATE_ON) - - if self.color_and_temp_supported: - color_mode = attributes.get(ATTR_COLOR_MODE) - color_temp_mode = color_mode == COLOR_MODE_COLOR_TEMP - primary_on_value = char_on_value if not color_temp_mode else 0 - secondary_on_value = char_on_value if color_temp_mode else 0 - self.char_on_primary.set_value(primary_on_value) - self.char_on_secondary.set_value(secondary_on_value) - else: - self.char_on_primary.set_value(char_on_value) + self.char_on.set_value(int(state == STATE_ON)) # Handle Brightness - if self.is_brightness_supported: + if self.brightness_supported: brightness = attributes.get(ATTR_BRIGHTNESS) if isinstance(brightness, (int, float)): brightness = round(brightness / 255 * 100, 0) @@ -227,22 +189,25 @@ class Light(HomeAccessory): # order to avoid this incorrect behavior. if brightness == 0 and state == STATE_ON: brightness = 1 - self.char_brightness_primary.set_value(brightness) - if self.color_and_temp_supported: - self.char_brightness_secondary.set_value(brightness) + self.char_brightness.set_value(brightness) + + # Handle Color - color must always be set before color temperature + # or the iOS UI will not display it correctly. + if self.color_supported: + if ATTR_COLOR_TEMP in attributes: + hue, saturation = color_temperature_to_hs( + color_temperature_mired_to_kelvin( + new_state.attributes[ATTR_COLOR_TEMP] + ) + ) + else: + hue, saturation = attributes.get(ATTR_HS_COLOR, (None, None)) + if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)): + self.char_hue.set_value(round(hue, 0)) + self.char_saturation.set_value(round(saturation, 0)) # Handle color temperature - if self.is_color_temp_supported: - color_temperature = attributes.get(ATTR_COLOR_TEMP) - if isinstance(color_temperature, (int, float)): - color_temperature = round(color_temperature, 0) - self.char_color_temperature.set_value(color_temperature) - - # Handle Color - if self.is_color_supported: - hue, saturation = attributes.get(ATTR_HS_COLOR, (None, None)) - if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)): - hue = round(hue, 0) - saturation = round(saturation, 0) - self.char_hue.set_value(hue) - self.char_saturation.set_value(saturation) + if self.color_temp_supported: + color_temp = attributes.get(ATTR_COLOR_TEMP) + if isinstance(color_temp, (int, float)): + self.char_color_temp.set_value(round(color_temp, 0)) diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index f75e6bf19ac..90e3aa0cabe 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -1,21 +1,21 @@ """Test different accessory types: Lights.""" +from datetime import timedelta + 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 -from homeassistant.components.homekit.type_lights import Light +from homeassistant.components.homekit.type_lights import ( + CHANGE_COALESCE_TIME_WINDOW, + Light, +) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, - ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_SUPPORTED_COLOR_MODES, - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_HS, - COLOR_MODE_RGB, - COLOR_MODE_XY, DOMAIN, ) from homeassistant.const import ( @@ -29,8 +29,16 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState from homeassistant.helpers import entity_registry as er +import homeassistant.util.dt as dt_util -from tests.common import async_mock_service +from tests.common import async_fire_time_changed, async_mock_service + + +async def _wait_for_light_coalesce(hass): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=CHANGE_COALESCE_TIME_WINDOW) + ) + await hass.async_block_till_done() async def test_light_basic(hass, hk_driver, events): @@ -44,45 +52,41 @@ async def test_light_basic(hass, hk_driver, events): assert acc.aid == 1 assert acc.category == 5 # Lightbulb - assert acc.char_on_primary.value + assert acc.char_on.value await acc.run() await hass.async_block_till_done() - assert acc.char_on_primary.value == 1 + assert acc.char_on.value == 1 hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - assert acc.char_on_primary.value == 0 + assert acc.char_on.value == 0 hass.states.async_set(entity_id, STATE_UNKNOWN) await hass.async_block_till_done() - assert acc.char_on_primary.value == 0 + assert acc.char_on.value == 0 hass.states.async_remove(entity_id) await hass.async_block_till_done() - assert acc.char_on_primary.value == 0 + assert acc.char_on.value == 0 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") call_turn_off = async_mock_service(hass, DOMAIN, "turn_off") - char_on_primary_iid = acc.char_on_primary.to_HAP()[HAP_REPR_IID] + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] hk_driver.set_characteristics( { HAP_REPR_CHARS: [ - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_on_primary_iid, - HAP_REPR_VALUE: 1, - } + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1} ] }, "mock_addr", ) - await hass.async_add_executor_job(acc.char_on_primary.client_update_value, 1) - await hass.async_block_till_done() + acc.char_on.client_update_value(1) + await _wait_for_light_coalesce(hass) assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 1 @@ -94,16 +98,12 @@ async def test_light_basic(hass, hk_driver, events): hk_driver.set_characteristics( { HAP_REPR_CHARS: [ - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_on_primary_iid, - HAP_REPR_VALUE: 0, - } + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 0} ] }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 2 @@ -128,17 +128,17 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): # 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_primary.value != 0 - char_on_primary_iid = acc.char_on_primary.to_HAP()[HAP_REPR_IID] - char_brightness_primary_iid = acc.char_brightness_primary.to_HAP()[HAP_REPR_IID] + 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 acc.run() await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 100 + 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_primary.value == 40 + assert acc.char_brightness.value == 40 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") @@ -147,21 +147,17 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): hk_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_on_primary_iid, - HAP_REPR_VALUE: 1, - }, - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_primary_iid, + HAP_REPR_IID: char_brightness_iid, HAP_REPR_VALUE: 20, }, ] }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) 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 @@ -173,21 +169,17 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): hk_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_on_primary_iid, - HAP_REPR_VALUE: 1, - }, - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_primary_iid, + HAP_REPR_IID: char_brightness_iid, HAP_REPR_VALUE: 40, }, ] }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) 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 @@ -199,21 +191,17 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): hk_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_on_primary_iid, - HAP_REPR_VALUE: 1, - }, - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_primary_iid, + HAP_REPR_IID: char_brightness_iid, HAP_REPR_VALUE: 0, }, ] }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 3 @@ -223,24 +211,24 @@ async def test_light_brightness(hass, hk_driver, events, supported_color_modes): # in update_state hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 1 + 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_primary.value == 100 + 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_primary.value == 1 + assert acc.char_brightness.value == 1 # Ensure floats are handled hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 55.66}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 22 + assert acc.char_brightness.value == 22 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 108.4}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 43 + assert acc.char_brightness.value == 43 hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0.0}) await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 1 + assert acc.char_brightness.value == 1 async def test_light_color_temperature(hass, hk_driver, events): @@ -256,33 +244,30 @@ async def test_light_color_temperature(hass, hk_driver, events): acc = Light(hass, hk_driver, "Light", entity_id, 1, None) hk_driver.add_accessory(acc) - assert acc.char_color_temperature.value == 190 + assert acc.char_color_temp.value == 190 await acc.run() await hass.async_block_till_done() - assert acc.char_color_temperature.value == 190 + assert acc.char_color_temp.value == 190 # 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] + 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_color_temperature_iid, + HAP_REPR_IID: char_color_temp_iid, HAP_REPR_VALUE: 250, } ] }, "mock_addr", ) - await hass.async_add_executor_job( - acc.char_color_temperature.client_update_value, 250 - ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250 @@ -292,11 +277,7 @@ async def test_light_color_temperature(hass, hk_driver, events): @pytest.mark.parametrize( "supported_color_modes", - [ - [COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS], - [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGB], - [COLOR_MODE_COLOR_TEMP, COLOR_MODE_XY], - ], + [["color_temp", "hs"], ["color_temp", "rgb"], ["color_temp", "xy"]], ) async def test_light_color_temperature_and_rgb_color( hass, hk_driver, events, supported_color_modes @@ -310,93 +291,190 @@ async def test_light_color_temperature_and_rgb_color( { ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, ATTR_COLOR_TEMP: 190, - ATTR_BRIGHTNESS: 255, - ATTR_COLOR_MODE: COLOR_MODE_RGB, ATTR_HS_COLOR: (260, 90), }, ) await hass.async_block_till_done() acc = Light(hass, hk_driver, "Light", entity_id, 1, None) - assert acc.char_hue.value == 260 - assert acc.char_saturation.value == 90 - assert acc.char_on_primary.value == 1 - assert acc.char_on_secondary.value == 0 - assert acc.char_brightness_primary.value == 100 - assert acc.char_brightness_secondary.value == 100 - - assert hasattr(acc, "char_color_temperature") - - hass.states.async_set( - entity_id, - STATE_ON, - { - ATTR_COLOR_TEMP: 224, - ATTR_COLOR_MODE: COLOR_MODE_COLOR_TEMP, - ATTR_BRIGHTNESS: 127, - }, - ) - await hass.async_block_till_done() - await acc.run() - await hass.async_block_till_done() - assert acc.char_color_temperature.value == 224 - assert acc.char_on_primary.value == 0 - assert acc.char_on_secondary.value == 1 - assert acc.char_brightness_primary.value == 50 - assert acc.char_brightness_secondary.value == 50 - - hass.states.async_set( - entity_id, - STATE_ON, - { - ATTR_COLOR_TEMP: 352, - ATTR_COLOR_MODE: COLOR_MODE_COLOR_TEMP, - }, - ) - await hass.async_block_till_done() - await acc.run() - await hass.async_block_till_done() - assert acc.char_color_temperature.value == 352 - assert acc.char_on_primary.value == 0 - assert acc.char_on_secondary.value == 1 hk_driver.add_accessory(acc) + assert acc.char_color_temp.value == 190 + assert acc.char_hue.value == 27 + assert acc.char_saturation.value == 16 + + assert hasattr(acc, "char_color_temp") + + hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 224}) + await hass.async_block_till_done() + await acc.run() + await hass.async_block_till_done() + assert acc.char_color_temp.value == 224 + assert acc.char_hue.value == 27 + assert acc.char_saturation.value == 27 + + hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 352}) + await hass.async_block_till_done() + await acc.run() + await hass.async_block_till_done() + assert acc.char_color_temp.value == 352 + assert acc.char_hue.value == 28 + assert acc.char_saturation.value == 61 + + 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] - char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID] + char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID] + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + + hk_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_temp_iid, + HAP_REPR_VALUE: 250, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_hue_iid, + HAP_REPR_VALUE: 50, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 50, + }, + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + 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{PERCENTAGE}, color temperature at 250" + ) + + # Only set Hue 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_VALUE: 30, + } ] }, "mock_addr", ) - assert acc.char_hue.value == 145 - assert acc.char_saturation.value == 75 + await _wait_for_light_coalesce(hass) + assert call_turn_on[1] + assert call_turn_on[1].data[ATTR_HS_COLOR] == (30, 50) + assert events[-1].data[ATTR_VALUE] == "set color at (30, 50)" + + # Only set Saturation hk_driver.set_characteristics( { HAP_REPR_CHARS: [ { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_color_temperature_iid, - HAP_REPR_VALUE: 200, - }, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 20, + } ] }, "mock_addr", ) - assert acc.char_color_temperature.value == 200 + await _wait_for_light_coalesce(hass) + assert call_turn_on[2] + assert call_turn_on[2].data[ATTR_HS_COLOR] == (30, 20) + + assert events[-1].data[ATTR_VALUE] == "set color at (30, 20)" + + # Generate a conflict by setting hue and then color temp + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_hue_iid, + HAP_REPR_VALUE: 80, + } + ] + }, + "mock_addr", + ) + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temp_iid, + HAP_REPR_VALUE: 320, + } + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on[3] + assert call_turn_on[3].data[ATTR_COLOR_TEMP] == 320 + assert events[-1].data[ATTR_VALUE] == "color temperature at 320" + + # Generate a conflict by setting color temp then saturation + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temp_iid, + HAP_REPR_VALUE: 404, + } + ] + }, + "mock_addr", + ) + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 35, + } + ] + }, + "mock_addr", + ) + await _wait_for_light_coalesce(hass) + assert call_turn_on[4] + assert call_turn_on[4].data[ATTR_HS_COLOR] == (80, 35) + assert events[-1].data[ATTR_VALUE] == "set color at (80, 35)" + + # Set from HASS + hass.states.async_set(entity_id, STATE_ON, {ATTR_HS_COLOR: (100, 100)}) + await hass.async_block_till_done() + await acc.run() + await hass.async_block_till_done() + assert acc.char_color_temp.value == 404 + assert acc.char_hue.value == 100 + assert acc.char_saturation.value == 100 @pytest.mark.parametrize("supported_color_modes", [["hs"], ["rgb"], ["xy"]]) @@ -444,7 +522,7 @@ async def test_light_rgb_color(hass, hk_driver, events, supported_color_modes): }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_HS_COLOR] == (145, 75) @@ -476,13 +554,13 @@ async def test_light_restore(hass, hk_driver, events): hk_driver.add_accessory(acc) assert acc.category == 5 # Lightbulb - assert acc.chars_primary == [] - assert acc.char_on_primary.value == 0 + assert acc.chars == [] + assert acc.char_on.value == 0 acc = Light(hass, hk_driver, "Light", "light.all_info_set", 2, None) assert acc.category == 5 # Lightbulb - assert acc.chars_primary == ["Brightness"] - assert acc.char_on_primary.value == 0 + assert acc.chars == ["Brightness"] + assert acc.char_on.value == 0 async def test_light_set_brightness_and_color(hass, hk_driver, events): @@ -503,19 +581,19 @@ async def test_light_set_brightness_and_color(hass, hk_driver, events): # 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_primary.value != 0 - char_on_primary_iid = acc.char_on_primary.to_HAP()[HAP_REPR_IID] - char_brightness_primary_iid = acc.char_brightness_primary.to_HAP()[HAP_REPR_IID] + 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 acc.run() await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 100 + 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_primary.value == 40 + assert acc.char_brightness.value == 40 hass.states.async_set(entity_id, STATE_ON, {ATTR_HS_COLOR: (4.5, 9.2)}) await hass.async_block_till_done() @@ -528,14 +606,10 @@ async def test_light_set_brightness_and_color(hass, hk_driver, events): hk_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_on_primary_iid, - HAP_REPR_VALUE: 1, - }, - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_primary_iid, + HAP_REPR_IID: char_brightness_iid, HAP_REPR_VALUE: 20, }, { @@ -552,7 +626,7 @@ async def test_light_set_brightness_and_color(hass, hk_driver, events): }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) 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 @@ -583,22 +657,22 @@ async def test_light_set_brightness_and_color_temp(hass, hk_driver, events): # 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_primary.value != 0 - char_on_primary_iid = acc.char_on_primary.to_HAP()[HAP_REPR_IID] - char_brightness_primary_iid = acc.char_brightness_primary.to_HAP()[HAP_REPR_IID] - char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID] + 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_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID] await acc.run() await hass.async_block_till_done() - assert acc.char_brightness_primary.value == 100 + 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_primary.value == 40 + assert acc.char_brightness.value == 40 hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: (224.14)}) await hass.async_block_till_done() - assert acc.char_color_temperature.value == 224 + assert acc.char_color_temp.value == 224 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") @@ -606,26 +680,22 @@ async def test_light_set_brightness_and_color_temp(hass, hk_driver, events): hk_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_on_primary_iid, - HAP_REPR_VALUE: 1, - }, - { - HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_brightness_primary_iid, + HAP_REPR_IID: char_brightness_iid, HAP_REPR_VALUE: 20, }, { HAP_REPR_AID: acc.aid, - HAP_REPR_IID: char_color_temperature_iid, + HAP_REPR_IID: char_color_temp_iid, HAP_REPR_VALUE: 250, }, ] }, "mock_addr", ) - await hass.async_block_till_done() + await _wait_for_light_coalesce(hass) 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