From 236738c455b827413ba052b1ea3bb24214d9f5db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Jul 2021 10:17:58 -1000 Subject: [PATCH] Add support for tilt only covers to HomeKit (#53130) --- .../components/homekit/accessories.py | 2 +- .../components/homekit/type_covers.py | 19 ++++++- .../homekit/test_get_accessories.py | 20 ++++++- tests/components/homekit/test_type_covers.py | 57 +++++++++++++++++-- 4 files changed, 86 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index b9bd62246cf..3c843955222 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -127,7 +127,7 @@ def get_accessory(hass, driver, state, aid, config): # noqa: C901 and features & cover.SUPPORT_SET_POSITION ): a_type = "Window" - elif features & cover.SUPPORT_SET_POSITION: + elif features & (cover.SUPPORT_SET_POSITION | cover.SUPPORT_SET_TILT_POSITION): a_type = "WindowCovering" elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): a_type = "WindowCoveringBasic" diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index f21287b3bf8..099eced62d3 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -13,6 +13,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, DOMAIN, + SUPPORT_SET_POSITION, SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, ) @@ -53,6 +54,8 @@ from .const import ( HK_POSITION_GOING_TO_MAX, HK_POSITION_GOING_TO_MIN, HK_POSITION_STOPPED, + PROP_MAX_VALUE, + PROP_MIN_VALUE, SERV_GARAGE_DOOR_OPENER, SERV_WINDOW, SERV_WINDOW_COVERING, @@ -273,12 +276,24 @@ class OpeningDevice(OpeningDeviceBase, HomeAccessory): """Initialize a WindowCovering accessory object.""" super().__init__(*args, category=category, service=service) state = self.hass.states.get(self.entity_id) - self.char_current_position = self.serv_cover.configure_char( CHAR_CURRENT_POSITION, value=0 ) + target_args = {"value": 0} + if self.features & SUPPORT_SET_POSITION: + target_args["setter_callback"] = self.move_cover + else: + # If its tilt only we lock the position state to 0 (closed) + # since CHAR_CURRENT_POSITION/CHAR_TARGET_POSITION are required + # by homekit, but really don't exist. + _LOGGER.debug( + "%s does not support setting position, current position will be locked to closed", + self.entity_id, + ) + target_args["properties"] = {PROP_MIN_VALUE: 0, PROP_MAX_VALUE: 0} + self.char_target_position = self.serv_cover.configure_char( - CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover + CHAR_TARGET_POSITION, **target_args ) self.char_position_state = self.serv_cover.configure_char( CHAR_POSITION_STATE, value=HK_POSITION_STOPPED diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 491d686162d..1b220153195 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -126,14 +126,28 @@ def test_types(type_name, entity_id, state, attrs, config): "Window", "cover.set_position", "open", - {ATTR_DEVICE_CLASS: "window", ATTR_SUPPORTED_FEATURES: 4}, + { + ATTR_DEVICE_CLASS: "window", + ATTR_SUPPORTED_FEATURES: cover.SUPPORT_SET_POSITION, + }, + ), + ( + "WindowCovering", + "cover.set_position", + "open", + {ATTR_SUPPORTED_FEATURES: cover.SUPPORT_SET_POSITION}, + ), + ( + "WindowCovering", + "cover.tilt", + "open", + {ATTR_SUPPORTED_FEATURES: cover.SUPPORT_SET_TILT_POSITION}, ), - ("WindowCovering", "cover.set_position", "open", {ATTR_SUPPORTED_FEATURES: 4}), ( "WindowCoveringBasic", "cover.open_window", "open", - {ATTR_SUPPORTED_FEATURES: 3}, + {ATTR_SUPPORTED_FEATURES: (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE)}, ), ], ) diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 8514801687c..89407edfbef 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -18,6 +18,8 @@ from homeassistant.components.homekit.const import ( HK_DOOR_CLOSING, HK_DOOR_OPEN, HK_DOOR_OPENING, + PROP_MAX_VALUE, + PROP_MIN_VALUE, ) from homeassistant.components.homekit.type_covers import ( GarageDoorOpener, @@ -133,7 +135,9 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "cover.window" - hass.states.async_set(entity_id, None) + hass.states.async_set( + entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION} + ) await hass.async_block_till_done() acc = WindowCovering(hass, hk_driver, "Cover", entity_id, 2, None) await acc.run() @@ -145,31 +149,51 @@ async def test_windowcovering_set_cover_position(hass, hk_driver, events): assert acc.char_current_position.value == 0 assert acc.char_target_position.value == 0 - hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_CURRENT_POSITION: None}) + hass.states.async_set( + entity_id, + STATE_UNKNOWN, + {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: None}, + ) await hass.async_block_till_done() assert acc.char_current_position.value == 0 assert acc.char_target_position.value == 0 assert acc.char_position_state.value == 2 - hass.states.async_set(entity_id, STATE_OPENING, {ATTR_CURRENT_POSITION: 60}) + hass.states.async_set( + entity_id, + STATE_OPENING, + {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 60}, + ) await hass.async_block_till_done() assert acc.char_current_position.value == 60 assert acc.char_target_position.value == 60 assert acc.char_position_state.value == 1 - hass.states.async_set(entity_id, STATE_OPENING, {ATTR_CURRENT_POSITION: 70.0}) + hass.states.async_set( + entity_id, + STATE_OPENING, + {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 70.0}, + ) await hass.async_block_till_done() assert acc.char_current_position.value == 70 assert acc.char_target_position.value == 70 assert acc.char_position_state.value == 1 - hass.states.async_set(entity_id, STATE_CLOSING, {ATTR_CURRENT_POSITION: 50}) + hass.states.async_set( + entity_id, + STATE_CLOSING, + {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 50}, + ) await hass.async_block_till_done() assert acc.char_current_position.value == 50 assert acc.char_target_position.value == 50 assert acc.char_position_state.value == 0 - hass.states.async_set(entity_id, STATE_OPEN, {ATTR_CURRENT_POSITION: 50}) + hass.states.async_set( + entity_id, + STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_POSITION, ATTR_CURRENT_POSITION: 50}, + ) await hass.async_block_till_done() assert acc.char_current_position.value == 50 assert acc.char_target_position.value == 50 @@ -283,6 +307,27 @@ async def test_windowcovering_cover_set_tilt(hass, hk_driver, events): assert events[-1].data[ATTR_VALUE] == 75 +async def test_windowcovering_tilt_only(hass, hk_driver, events): + """Test we lock the window covering closed when its tilt only.""" + entity_id = "cover.window" + + hass.states.async_set( + entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_TILT_POSITION} + ) + await hass.async_block_till_done() + acc = WindowCovering(hass, hk_driver, "Cover", entity_id, 2, None) + await acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 14 # WindowCovering + + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + assert acc.char_target_position.properties[PROP_MIN_VALUE] == 0 + assert acc.char_target_position.properties[PROP_MAX_VALUE] == 0 + + async def test_windowcovering_open_close(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "cover.window"