Support Garage Doors in HomeKit (#13796)

This commit is contained in:
Mark Coombes 2018-04-12 12:08:48 -04:00 committed by cdce8p
parent 51bdd06d1f
commit 993866a314
5 changed files with 132 additions and 7 deletions

View file

@ -8,7 +8,8 @@ from zlib import adler32
import voluptuous as vol
from homeassistant.components.cover import SUPPORT_SET_POSITION
from homeassistant.components.cover import (
SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION)
from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
ATTR_DEVICE_CLASS, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT,
@ -92,9 +93,13 @@ def get_accessory(hass, state, aid, config):
a_type = 'Thermostat'
elif state.domain == 'cover':
# Only add covers that support set_cover_position
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if features & SUPPORT_SET_POSITION:
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
if device_class == 'garage' and \
features & (SUPPORT_OPEN | SUPPORT_CLOSE):
a_type = 'GarageDoorOpener'
elif features & SUPPORT_SET_POSITION:
a_type = 'WindowCovering'
elif state.domain == 'light':

View file

@ -24,6 +24,7 @@ MANUFACTURER = 'HomeAssistant'
# #### Categories ####
CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM'
CATEGORY_GARAGE_DOOR_OPENER = 'GARAGE_DOOR_OPENER'
CATEGORY_LIGHT = 'LIGHTBULB'
CATEGORY_LOCK = 'DOOR_LOCK'
CATEGORY_SENSOR = 'SENSOR'
@ -38,6 +39,7 @@ SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor'
SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor'
SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor'
SERV_CONTACT_SENSOR = 'ContactSensor'
SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener'
SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity
SERV_LEAK_SENSOR = 'LeakSensor'
SERV_LIGHT_SENSOR = 'LightSensor'
@ -65,6 +67,7 @@ CHAR_COLOR_TEMPERATURE = 'ColorTemperature'
CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState'
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel'
CHAR_CURRENT_DOOR_STATE = 'CurrentDoorState'
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100]
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent
@ -85,6 +88,7 @@ CHAR_ON = 'On' # boolean
CHAR_SATURATION = 'Saturation' # percent
CHAR_SERIAL_NUMBER = 'SerialNumber'
CHAR_SMOKE_DETECTED = 'SmokeDetected'
CHAR_TARGET_DOOR_STATE = 'TargetDoorState'
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
CHAR_TARGET_POSITION = 'TargetPosition' # Int | [0, 100]
CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState'

View file

@ -3,17 +3,63 @@ import logging
from homeassistant.components.cover import (
ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN)
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_CLOSED)
from . import TYPES
from .accessories import HomeAccessory, add_preload_service, setup_char
from .const import (
CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING,
CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION)
CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION,
CATEGORY_GARAGE_DOOR_OPENER, SERV_GARAGE_DOOR_OPENER,
CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE)
_LOGGER = logging.getLogger(__name__)
@TYPES.register('GarageDoorOpener')
class GarageDoorOpener(HomeAccessory):
"""Generate a Garage Door Opener accessory for a cover entity.
The cover entity must be in the 'garage' device class
and support no more than open, close, and stop.
"""
def __init__(self, *args, config):
"""Initialize a GarageDoorOpener accessory object."""
super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER)
self.flag_target_state = False
serv_garage_door = add_preload_service(self, SERV_GARAGE_DOOR_OPENER)
self.char_current_state = setup_char(
CHAR_CURRENT_DOOR_STATE, serv_garage_door, value=0)
self.char_target_state = setup_char(
CHAR_TARGET_DOOR_STATE, serv_garage_door, value=0,
callback=self.set_state)
def set_state(self, value):
"""Change garage state if call came from HomeKit."""
_LOGGER.debug('%s: Set state to %d', self.entity_id, value)
self.flag_target_state = True
if value == 0:
self.char_current_state.set_value(3)
self.hass.components.cover.open_cover(self.entity_id)
elif value == 1:
self.char_current_state.set_value(2)
self.hass.components.cover.close_cover(self.entity_id)
def update_state(self, new_state):
"""Update cover state after state changed."""
hass_state = new_state.state
if hass_state in (STATE_OPEN, STATE_CLOSED):
current_state = 0 if hass_state == STATE_OPEN else 1
self.char_current_state.set_value(current_state)
if not self.flag_target_state:
self.char_target_state.set_value(current_state)
self.flag_target_state = False
@TYPES.register('WindowCovering')
class WindowCovering(HomeAccessory):
"""Generate a Window accessory for a cover entity.

View file

@ -4,6 +4,8 @@ import unittest
from unittest.mock import patch, Mock
from homeassistant.core import State
from homeassistant.components.cover import (
SUPPORT_OPEN, SUPPORT_CLOSE)
from homeassistant.components.climate import (
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.components.homekit import get_accessory, TYPES
@ -136,6 +138,15 @@ class TestGetAccessories(unittest.TestCase):
state = State('device_tracker.someone', 'not_home', {})
get_accessory(None, state, 2, {})
def test_garage_door(self):
"""Test cover with device_class: 'garage' and required features."""
with patch.dict(TYPES, {'GarageDoorOpener': self.mock_type}):
state = State('cover.garage_door', 'open', {
ATTR_DEVICE_CLASS: 'garage',
ATTR_SUPPORTED_FEATURES:
SUPPORT_OPEN | SUPPORT_CLOSE})
get_accessory(None, state, 2, {})
def test_cover_set_position(self):
"""Test cover with support for set_cover_position."""
with patch.dict(TYPES, {'WindowCovering': self.mock_type}):

View file

@ -4,9 +4,10 @@ import unittest
from homeassistant.core import callback
from homeassistant.components.cover import (
ATTR_POSITION, ATTR_CURRENT_POSITION)
from homeassistant.components.homekit.type_covers import WindowCovering
from homeassistant.components.homekit.type_covers import (
GarageDoorOpener, WindowCovering)
from homeassistant.const import (
STATE_UNKNOWN, STATE_OPEN,
STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_OPEN,
ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE)
from tests.common import get_test_home_assistant
@ -31,6 +32,64 @@ class TestHomekitSensors(unittest.TestCase):
"""Stop down everything that was started."""
self.hass.stop()
def test_garage_door_open_close(self):
"""Test if accessory and HA are updated accordingly."""
garage_door = 'cover.garage_door'
acc = GarageDoorOpener(self.hass, 'Cover', garage_door, 2, config=None)
acc.run()
self.assertEqual(acc.aid, 2)
self.assertEqual(acc.category, 4) # GarageDoorOpener
self.assertEqual(acc.char_current_state.value, 0)
self.assertEqual(acc.char_target_state.value, 0)
self.hass.states.set(garage_door, STATE_CLOSED)
self.hass.block_till_done()
self.assertEqual(acc.char_current_state.value, 1)
self.assertEqual(acc.char_target_state.value, 1)
self.hass.states.set(garage_door, STATE_OPEN)
self.hass.block_till_done()
self.assertEqual(acc.char_current_state.value, 0)
self.assertEqual(acc.char_target_state.value, 0)
self.hass.states.set(garage_door, STATE_UNAVAILABLE)
self.hass.block_till_done()
self.assertEqual(acc.char_current_state.value, 0)
self.assertEqual(acc.char_target_state.value, 0)
self.hass.states.set(garage_door, STATE_UNKNOWN)
self.hass.block_till_done()
self.assertEqual(acc.char_current_state.value, 0)
self.assertEqual(acc.char_target_state.value, 0)
# Set closed from HomeKit
acc.char_target_state.client_update_value(1)
self.hass.block_till_done()
self.assertEqual(acc.char_current_state.value, 2)
self.assertEqual(acc.char_target_state.value, 1)
self.assertEqual(
self.events[0].data[ATTR_SERVICE], 'close_cover')
self.hass.states.set(garage_door, STATE_CLOSED)
self.hass.block_till_done()
# Set open from HomeKit
acc.char_target_state.client_update_value(0)
self.hass.block_till_done()
self.assertEqual(acc.char_current_state.value, 3)
self.assertEqual(acc.char_target_state.value, 0)
self.assertEqual(
self.events[1].data[ATTR_SERVICE], 'open_cover')
def test_window_set_cover_position(self):
"""Test if accessory and HA are updated accordingly."""
window_cover = 'cover.window'