Support basic covers with open/close/stop services HomeKit (#13819)

* Support basic covers with open/close/stop services
* Support optional stop
* Tests
pull/13798/merge
Nick Whyte 2018-04-18 22:39:58 +10:00 committed by cdce8p
parent 23b97b9105
commit b589dbf26c
5 changed files with 189 additions and 8 deletions

View File

@ -101,6 +101,8 @@ def get_accessory(hass, state, aid, config):
a_type = 'GarageDoorOpener'
elif features & SUPPORT_SET_POSITION:
a_type = 'WindowCovering'
elif features & (SUPPORT_OPEN | SUPPORT_CLOSE):
a_type = 'WindowCoveringBasic'
elif state.domain == 'light':
a_type = 'Light'

View File

@ -52,7 +52,8 @@ SERV_SMOKE_SENSOR = 'SmokeSensor'
SERV_SWITCH = 'Switch'
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
SERV_THERMOSTAT = 'Thermostat'
SERV_WINDOW_COVERING = 'WindowCovering' # CurrentPosition, TargetPosition
SERV_WINDOW_COVERING = 'WindowCovering'
# CurrentPosition, TargetPosition, PositionState
# #### Characteristics ####
@ -85,6 +86,7 @@ CHAR_MOTION_DETECTED = 'MotionDetected'
CHAR_NAME = 'Name'
CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected'
CHAR_ON = 'On' # boolean
CHAR_POSITION_STATE = 'PositionState'
CHAR_SATURATION = 'Saturation' # percent
CHAR_SERIAL_NUMBER = 'SerialNumber'
CHAR_SMOKE_DETECTED = 'SmokeDetected'

View File

@ -2,15 +2,17 @@
import logging
from homeassistant.components.cover import (
ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN)
ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP)
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_CLOSED)
ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_CLOSED,
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_STOP_COVER,
ATTR_SUPPORTED_FEATURES)
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, CHAR_POSITION_STATE,
CATEGORY_GARAGE_DOOR_OPENER, SERV_GARAGE_DOOR_OPENER,
CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE)
@ -96,3 +98,62 @@ class WindowCovering(HomeAccessory):
abs(current_position - self.homekit_target) < 6:
self.char_target_position.set_value(current_position)
self.homekit_target = None
@TYPES.register('WindowCoveringBasic')
class WindowCoveringBasic(HomeAccessory):
"""Generate a Window accessory for a cover entity.
The cover entity must support: open_cover, close_cover,
stop_cover (optional).
"""
def __init__(self, *args, config):
"""Initialize a WindowCovering accessory object."""
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
features = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_SUPPORTED_FEATURES)
self.supports_stop = features & SUPPORT_STOP
serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
self.char_current_position = setup_char(
CHAR_CURRENT_POSITION, serv_cover, value=0)
self.char_target_position = setup_char(
CHAR_TARGET_POSITION, serv_cover, value=0,
callback=self.move_cover)
self.char_position_state = setup_char(
CHAR_POSITION_STATE, serv_cover, value=2)
def move_cover(self, value):
"""Move cover to value if call came from HomeKit."""
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
if self.supports_stop:
if value > 70:
service, position = (SERVICE_OPEN_COVER, 100)
elif value < 30:
service, position = (SERVICE_CLOSE_COVER, 0)
else:
service, position = (SERVICE_STOP_COVER, 50)
else:
if value >= 50:
service, position = (SERVICE_OPEN_COVER, 100)
else:
service, position = (SERVICE_CLOSE_COVER, 0)
self.hass.services.call(DOMAIN, service,
{ATTR_ENTITY_ID: self.entity_id})
# Snap the current/target position to the expected final position.
self.char_current_position.set_value(position)
self.char_target_position.set_value(position)
self.char_position_state.set_value(2)
def update_state(self, new_state):
"""Update cover position after state changed."""
position_mapping = {STATE_OPEN: 100, STATE_CLOSED: 0}
hk_position = position_mapping.get(new_state.state)
if hk_position is not None:
self.char_current_position.set_value(hk_position)
self.char_target_position.set_value(hk_position)
self.char_position_state.set_value(2)

View File

@ -154,6 +154,13 @@ class TestGetAccessories(unittest.TestCase):
{ATTR_SUPPORTED_FEATURES: 4})
get_accessory(None, state, 2, {})
def test_cover_open_close(self):
"""Test cover with support for open and close."""
with patch.dict(TYPES, {'WindowCoveringBasic': self.mock_type}):
state = State('cover.open_window', 'open',
{ATTR_SUPPORTED_FEATURES: 3})
get_accessory(None, state, 2, {})
def test_alarm_control_panel(self):
"""Test alarm control panel."""
config = {ATTR_CODE: '1234'}

View File

@ -3,12 +3,13 @@ import unittest
from homeassistant.core import callback
from homeassistant.components.cover import (
ATTR_POSITION, ATTR_CURRENT_POSITION)
ATTR_POSITION, ATTR_CURRENT_POSITION, SUPPORT_STOP)
from homeassistant.components.homekit.type_covers import (
GarageDoorOpener, WindowCovering)
GarageDoorOpener, WindowCovering, WindowCoveringBasic)
from homeassistant.const import (
STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_OPEN,
ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE)
ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE,
ATTR_SUPPORTED_FEATURES)
from tests.common import get_test_home_assistant
@ -132,9 +133,117 @@ class TestHomekitSensors(unittest.TestCase):
acc.char_target_position.client_update_value(75)
self.hass.block_till_done()
self.assertEqual(
self.events[0].data[ATTR_SERVICE], 'set_cover_position')
self.events[1].data[ATTR_SERVICE], 'set_cover_position')
self.assertEqual(
self.events[1].data[ATTR_SERVICE_DATA][ATTR_POSITION], 75)
self.assertEqual(acc.char_current_position.value, 50)
self.assertEqual(acc.char_target_position.value, 75)
def test_window_open_close(self):
"""Test if accessory and HA are updated accordingly."""
window_cover = 'cover.window'
self.hass.states.set(window_cover, STATE_UNKNOWN,
{ATTR_SUPPORTED_FEATURES: 0})
acc = WindowCoveringBasic(self.hass, 'Cover', window_cover, 2,
config=None)
acc.run()
self.assertEqual(acc.aid, 2)
self.assertEqual(acc.category, 14) # WindowCovering
self.assertEqual(acc.char_current_position.value, 0)
self.assertEqual(acc.char_target_position.value, 0)
self.assertEqual(acc.char_position_state.value, 2)
self.hass.states.set(window_cover, STATE_UNKNOWN)
self.hass.block_till_done()
self.assertEqual(acc.char_current_position.value, 0)
self.assertEqual(acc.char_target_position.value, 0)
self.assertEqual(acc.char_position_state.value, 2)
self.hass.states.set(window_cover, STATE_OPEN)
self.hass.block_till_done()
self.assertEqual(acc.char_current_position.value, 100)
self.assertEqual(acc.char_target_position.value, 100)
self.assertEqual(acc.char_position_state.value, 2)
self.hass.states.set(window_cover, STATE_CLOSED)
self.hass.block_till_done()
self.assertEqual(acc.char_current_position.value, 0)
self.assertEqual(acc.char_target_position.value, 0)
self.assertEqual(acc.char_position_state.value, 2)
# Set from HomeKit
acc.char_target_position.client_update_value(25)
self.hass.block_till_done()
self.assertEqual(
self.events[0].data[ATTR_SERVICE], 'close_cover')
self.assertEqual(acc.char_current_position.value, 0)
self.assertEqual(acc.char_target_position.value, 0)
self.assertEqual(acc.char_position_state.value, 2)
# Set from HomeKit
acc.char_target_position.client_update_value(90)
self.hass.block_till_done()
self.assertEqual(
self.events[1].data[ATTR_SERVICE], 'open_cover')
self.assertEqual(acc.char_current_position.value, 100)
self.assertEqual(acc.char_target_position.value, 100)
self.assertEqual(acc.char_position_state.value, 2)
# Set from HomeKit
acc.char_target_position.client_update_value(55)
self.hass.block_till_done()
self.assertEqual(
self.events[2].data[ATTR_SERVICE], 'open_cover')
self.assertEqual(acc.char_current_position.value, 100)
self.assertEqual(acc.char_target_position.value, 100)
self.assertEqual(acc.char_position_state.value, 2)
def test_window_open_close_stop(self):
"""Test if accessory and HA are updated accordingly."""
window_cover = 'cover.window'
self.hass.states.set(window_cover, STATE_UNKNOWN,
{ATTR_SUPPORTED_FEATURES: SUPPORT_STOP})
acc = WindowCoveringBasic(self.hass, 'Cover', window_cover, 2,
config=None)
acc.run()
# Set from HomeKit
acc.char_target_position.client_update_value(25)
self.hass.block_till_done()
self.assertEqual(
self.events[0].data[ATTR_SERVICE], 'close_cover')
self.assertEqual(acc.char_current_position.value, 0)
self.assertEqual(acc.char_target_position.value, 0)
self.assertEqual(acc.char_position_state.value, 2)
# Set from HomeKit
acc.char_target_position.client_update_value(90)
self.hass.block_till_done()
self.assertEqual(
self.events[1].data[ATTR_SERVICE], 'open_cover')
self.assertEqual(acc.char_current_position.value, 100)
self.assertEqual(acc.char_target_position.value, 100)
self.assertEqual(acc.char_position_state.value, 2)
# Set from HomeKit
acc.char_target_position.client_update_value(55)
self.hass.block_till_done()
self.assertEqual(
self.events[2].data[ATTR_SERVICE], 'stop_cover')
self.assertEqual(acc.char_current_position.value, 50)
self.assertEqual(acc.char_target_position.value, 50)
self.assertEqual(acc.char_position_state.value, 2)