From 4bf9ce6fca0fb40b4b38a69973453bccf6f61871 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 23 Oct 2020 12:18:02 -0500 Subject: [PATCH] Add support for homekit garage obstruction sensors (#42243) --- homeassistant/components/homekit/const.py | 4 ++ .../components/homekit/type_covers.py | 60 ++++++++++++++++- homeassistant/components/homekit/util.py | 13 ++++ .../components/homekit_controller/cover.py | 4 +- tests/components/homekit/test_type_covers.py | 65 ++++++++++++++++++- 5 files changed, 139 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 3a0ba43e9cb..77c5dbff0f9 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -28,6 +28,8 @@ ATTR_MANUFACTURER = "manufacturer" ATTR_MODEL = "model" ATTR_SOFTWARE_VERSION = "sw_version" ATTR_KEY_NAME = "key_name" +# Current attribute used by homekit_controller +ATTR_OBSTRUCTION_DETECTED = "obstruction-detected" # #### Config #### CONF_HOMEKIT_MODE = "mode" @@ -45,6 +47,7 @@ CONF_LINKED_BATTERY_CHARGING_SENSOR = "linked_battery_charging_sensor" CONF_LINKED_DOORBELL_SENSOR = "linked_doorbell_sensor" CONF_LINKED_MOTION_SENSOR = "linked_motion_sensor" CONF_LINKED_HUMIDITY_SENSOR = "linked_humidity_sensor" +CONF_LINKED_OBSTRUCTION_SENSOR = "linked_obstruction_sensor" CONF_LOW_BATTERY_THRESHOLD = "low_battery_threshold" CONF_MAX_FPS = "max_fps" CONF_MAX_HEIGHT = "max_height" @@ -192,6 +195,7 @@ CHAR_MODEL = "Model" CHAR_MOTION_DETECTED = "MotionDetected" CHAR_MUTE = "Mute" CHAR_NAME = "Name" +CHAR_OBSTRUCTION_DETECTED = "ObstructionDetected" CHAR_OCCUPANCY_DETECTED = "OccupancyDetected" CHAR_ON = "On" CHAR_OUTLET_IN_USE = "OutletInUse" diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index d8d8da5a974..cfac3d4f7de 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -26,21 +26,26 @@ from homeassistant.const import ( SERVICE_STOP_COVER, STATE_CLOSED, STATE_CLOSING, + STATE_ON, STATE_OPEN, STATE_OPENING, ) from homeassistant.core import callback +from homeassistant.helpers.event import async_track_state_change_event from .accessories import TYPES, HomeAccessory, debounce from .const import ( + ATTR_OBSTRUCTION_DETECTED, CHAR_CURRENT_DOOR_STATE, CHAR_CURRENT_POSITION, CHAR_CURRENT_TILT_ANGLE, CHAR_HOLD_POSITION, + CHAR_OBSTRUCTION_DETECTED, CHAR_POSITION_STATE, CHAR_TARGET_DOOR_STATE, CHAR_TARGET_POSITION, CHAR_TARGET_TILT_ANGLE, + CONF_LINKED_OBSTRUCTION_SENSOR, DEVICE_PRECISION_LEEWAY, HK_DOOR_CLOSED, HK_DOOR_CLOSING, @@ -74,7 +79,6 @@ DOOR_TARGET_HASS_TO_HK = { STATE_CLOSING: HK_DOOR_CLOSED, } - _LOGGER = logging.getLogger(__name__) @@ -98,8 +102,55 @@ class GarageDoorOpener(HomeAccessory): self.char_target_state = serv_garage_door.configure_char( CHAR_TARGET_DOOR_STATE, value=0, setter_callback=self.set_state ) + self.char_obstruction_detected = serv_garage_door.configure_char( + CHAR_OBSTRUCTION_DETECTED, value=False + ) + + self.linked_obstruction_sensor = self.config.get(CONF_LINKED_OBSTRUCTION_SENSOR) + if self.linked_obstruction_sensor: + self._async_update_obstruction_state( + self.hass.states.get(self.linked_obstruction_sensor) + ) + self.async_update_state(state) + async def run_handler(self): + """Handle accessory driver started event. + + Run inside the Home Assistant event loop. + """ + if self.linked_obstruction_sensor: + async_track_state_change_event( + self.hass, + [self.linked_obstruction_sensor], + self._async_update_obstruction_event, + ) + + await super().run_handler() + + @callback + def _async_update_obstruction_event(self, event): + """Handle state change event listener callback.""" + self._async_update_obstruction_state(event.data.get("new_state")) + + @callback + def _async_update_obstruction_state(self, new_state): + """Handle linked obstruction sensor state change to update HomeKit value.""" + if not new_state: + return + + detected = new_state.state == STATE_ON + if self.char_obstruction_detected.value == detected: + return + + self.char_obstruction_detected.set_value(detected) + _LOGGER.debug( + "%s: Set linked obstruction %s sensor to %d", + self.entity_id, + self.linked_obstruction_sensor, + detected, + ) + def set_state(self, value): """Change garage state if call came from HomeKit.""" _LOGGER.debug("%s: Set state to %d", self.entity_id, value) @@ -121,6 +172,13 @@ class GarageDoorOpener(HomeAccessory): target_door_state = DOOR_TARGET_HASS_TO_HK.get(hass_state) current_door_state = DOOR_CURRENT_HASS_TO_HK.get(hass_state) + if ATTR_OBSTRUCTION_DETECTED in new_state.attributes: + obstruction_detected = ( + new_state.attributes[ATTR_OBSTRUCTION_DETECTED] is True + ) + if self.char_obstruction_detected.value != obstruction_detected: + self.char_obstruction_detected.set_value(obstruction_detected) + if ( target_door_state is not None and self.char_target_state.value != target_door_state diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 2199371c00d..2ea4fcb551b 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -37,6 +37,7 @@ from .const import ( CONF_LINKED_DOORBELL_SENSOR, CONF_LINKED_HUMIDITY_SENSOR, CONF_LINKED_MOTION_SENSOR, + CONF_LINKED_OBSTRUCTION_SENSOR, CONF_LOW_BATTERY_THRESHOLD, CONF_MAX_FPS, CONF_MAX_HEIGHT, @@ -138,6 +139,15 @@ HUMIDIFIER_SCHEMA = BASIC_INFO_SCHEMA.extend( {vol.Optional(CONF_LINKED_HUMIDITY_SENSOR): cv.entity_domain(sensor.DOMAIN)} ) + +COVER_SCHEMA = BASIC_INFO_SCHEMA.extend( + { + vol.Optional(CONF_LINKED_OBSTRUCTION_SENSOR): cv.entity_domain( + binary_sensor.DOMAIN + ) + } +) + CODE_SCHEMA = BASIC_INFO_SCHEMA.extend( {vol.Optional(ATTR_CODE, default=None): vol.Any(None, cv.string)} ) @@ -247,6 +257,9 @@ def validate_entity_config(values): elif domain == "humidifier": config = HUMIDIFIER_SCHEMA(config) + elif domain == "cover": + config = COVER_SCHEMA(config) + else: config = BASIC_INFO_SCHEMA(config) diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index db824ea060d..a79ef5d1ee7 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -116,9 +116,7 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverEntity): obstruction_detected = self.service.value( CharacteristicsTypes.OBSTRUCTION_DETECTED ) - if not obstruction_detected: - return {} - return {"obstruction-detected": obstruction_detected} + return {"obstruction-detected": obstruction_detected is True} class HomeKitWindowCover(HomeKitEntity, CoverEntity): diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index cd193b61646..48b20e8e0b8 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -14,7 +14,9 @@ from homeassistant.components.cover import ( SUPPORT_STOP, ) from homeassistant.components.homekit.const import ( + ATTR_OBSTRUCTION_DETECTED, ATTR_VALUE, + CONF_LINKED_OBSTRUCTION_SENSOR, HK_DOOR_CLOSED, HK_DOOR_CLOSING, HK_DOOR_OPEN, @@ -27,6 +29,8 @@ from homeassistant.const import ( SERVICE_SET_COVER_TILT_POSITION, STATE_CLOSED, STATE_CLOSING, + STATE_OFF, + STATE_ON, STATE_OPEN, STATE_OPENING, STATE_UNAVAILABLE, @@ -76,20 +80,25 @@ async def test_garage_door_open_close(hass, hk_driver, cls, events): assert acc.char_current_state.value == HK_DOOR_OPEN assert acc.char_target_state.value == HK_DOOR_OPEN - hass.states.async_set(entity_id, STATE_CLOSED) + hass.states.async_set(entity_id, STATE_CLOSED, {ATTR_OBSTRUCTION_DETECTED: False}) await hass.async_block_till_done() assert acc.char_current_state.value == HK_DOOR_CLOSED assert acc.char_target_state.value == HK_DOOR_CLOSED + assert acc.char_obstruction_detected.value is False - hass.states.async_set(entity_id, STATE_OPEN) + hass.states.async_set(entity_id, STATE_OPEN, {ATTR_OBSTRUCTION_DETECTED: True}) await hass.async_block_till_done() assert acc.char_current_state.value == HK_DOOR_OPEN assert acc.char_target_state.value == HK_DOOR_OPEN + assert acc.char_obstruction_detected.value is True - hass.states.async_set(entity_id, STATE_UNAVAILABLE) + hass.states.async_set( + entity_id, STATE_UNAVAILABLE, {ATTR_OBSTRUCTION_DETECTED: False} + ) await hass.async_block_till_done() assert acc.char_current_state.value == HK_DOOR_OPEN assert acc.char_target_state.value == HK_DOOR_OPEN + assert acc.char_obstruction_detected.value is False hass.states.async_set(entity_id, STATE_UNKNOWN) await hass.async_block_till_done() @@ -528,3 +537,53 @@ async def test_windowcovering_restore(hass, hk_driver, cls, events): assert acc.char_current_position is not None assert acc.char_target_position is not None assert acc.char_position_state is not None + + +async def test_garage_door_with_linked_obstruction_sensor(hass, hk_driver, cls, events): + """Test if accessory and HA are updated accordingly with a linked obstruction sensor.""" + linked_obstruction_sensor_entity_id = "binary_sensor.obstruction" + entity_id = "cover.garage_door" + + hass.states.async_set(linked_obstruction_sensor_entity_id, STATE_OFF) + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = cls.garage( + hass, + hk_driver, + "Garage Door", + entity_id, + 2, + {CONF_LINKED_OBSTRUCTION_SENSOR: linked_obstruction_sensor_entity_id}, + ) + await acc.run_handler() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 4 # GarageDoorOpener + + assert acc.char_current_state.value == HK_DOOR_OPEN + assert acc.char_target_state.value == HK_DOOR_OPEN + + hass.states.async_set(entity_id, STATE_CLOSED) + await hass.async_block_till_done() + assert acc.char_current_state.value == HK_DOOR_CLOSED + assert acc.char_target_state.value == HK_DOOR_CLOSED + assert acc.char_obstruction_detected.value is False + + hass.states.async_set(entity_id, STATE_OPEN) + hass.states.async_set(linked_obstruction_sensor_entity_id, STATE_ON) + await hass.async_block_till_done() + assert acc.char_current_state.value == HK_DOOR_OPEN + assert acc.char_target_state.value == HK_DOOR_OPEN + assert acc.char_obstruction_detected.value is True + + hass.states.async_set(entity_id, STATE_CLOSED) + hass.states.async_set(linked_obstruction_sensor_entity_id, STATE_OFF) + await hass.async_block_till_done() + assert acc.char_current_state.value == HK_DOOR_CLOSED + assert acc.char_target_state.value == HK_DOOR_CLOSED + assert acc.char_obstruction_detected.value is False + + hass.states.async_remove(entity_id) + hass.states.async_remove(linked_obstruction_sensor_entity_id) + await hass.async_block_till_done()