Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions homeassistant/components/homekit/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
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"
Expand All @@ -45,6 +47,7 @@
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"
Expand Down Expand Up @@ -192,6 +195,7 @@
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"
Expand Down
60 changes: 59 additions & 1 deletion homeassistant/components/homekit/type_covers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,26 @@
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,
Expand Down Expand Up @@ -74,7 +79,6 @@
STATE_CLOSING: HK_DOOR_CLOSED,
}


_LOGGER = logging.getLogger(__name__)


Expand All @@ -98,8 +102,55 @@ def __init__(self, *args):
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)
Expand All @@ -121,6 +172,13 @@ def async_update_state(self, new_state):
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
Expand Down
13 changes: 13 additions & 0 deletions homeassistant/components/homekit/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
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,
Expand Down Expand Up @@ -138,6 +139,15 @@
{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)}
)
Expand Down Expand Up @@ -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)

Expand Down
4 changes: 1 addition & 3 deletions homeassistant/components/homekit_controller/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,7 @@ def device_state_attributes(self):
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):
Expand Down
65 changes: 62 additions & 3 deletions tests/components/homekit/test_type_covers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
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,
Expand All @@ -27,6 +29,8 @@
SERVICE_SET_COVER_TILT_POSITION,
STATE_CLOSED,
STATE_CLOSING,
STATE_OFF,
STATE_ON,
STATE_OPEN,
STATE_OPENING,
STATE_UNAVAILABLE,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()