mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Support basic covers with open/close/stop services HomeKit (#13819)
* Support basic covers with open/close/stop services * Support optional stop * Tests
This commit is contained in:
parent
23b97b9105
commit
b589dbf26c
@ -101,6 +101,8 @@ def get_accessory(hass, state, aid, config):
|
|||||||
a_type = 'GarageDoorOpener'
|
a_type = 'GarageDoorOpener'
|
||||||
elif features & SUPPORT_SET_POSITION:
|
elif features & SUPPORT_SET_POSITION:
|
||||||
a_type = 'WindowCovering'
|
a_type = 'WindowCovering'
|
||||||
|
elif features & (SUPPORT_OPEN | SUPPORT_CLOSE):
|
||||||
|
a_type = 'WindowCoveringBasic'
|
||||||
|
|
||||||
elif state.domain == 'light':
|
elif state.domain == 'light':
|
||||||
a_type = 'Light'
|
a_type = 'Light'
|
||||||
|
@ -52,7 +52,8 @@ SERV_SMOKE_SENSOR = 'SmokeSensor'
|
|||||||
SERV_SWITCH = 'Switch'
|
SERV_SWITCH = 'Switch'
|
||||||
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
|
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
|
||||||
SERV_THERMOSTAT = 'Thermostat'
|
SERV_THERMOSTAT = 'Thermostat'
|
||||||
SERV_WINDOW_COVERING = 'WindowCovering' # CurrentPosition, TargetPosition
|
SERV_WINDOW_COVERING = 'WindowCovering'
|
||||||
|
# CurrentPosition, TargetPosition, PositionState
|
||||||
|
|
||||||
|
|
||||||
# #### Characteristics ####
|
# #### Characteristics ####
|
||||||
@ -85,6 +86,7 @@ CHAR_MOTION_DETECTED = 'MotionDetected'
|
|||||||
CHAR_NAME = 'Name'
|
CHAR_NAME = 'Name'
|
||||||
CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected'
|
CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected'
|
||||||
CHAR_ON = 'On' # boolean
|
CHAR_ON = 'On' # boolean
|
||||||
|
CHAR_POSITION_STATE = 'PositionState'
|
||||||
CHAR_SATURATION = 'Saturation' # percent
|
CHAR_SATURATION = 'Saturation' # percent
|
||||||
CHAR_SERIAL_NUMBER = 'SerialNumber'
|
CHAR_SERIAL_NUMBER = 'SerialNumber'
|
||||||
CHAR_SMOKE_DETECTED = 'SmokeDetected'
|
CHAR_SMOKE_DETECTED = 'SmokeDetected'
|
||||||
|
@ -2,15 +2,17 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.cover import (
|
from homeassistant.components.cover import (
|
||||||
ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN)
|
ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP)
|
||||||
from homeassistant.const import (
|
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 . import TYPES
|
||||||
from .accessories import HomeAccessory, add_preload_service, setup_char
|
from .accessories import HomeAccessory, add_preload_service, setup_char
|
||||||
from .const import (
|
from .const import (
|
||||||
CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING,
|
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,
|
CATEGORY_GARAGE_DOOR_OPENER, SERV_GARAGE_DOOR_OPENER,
|
||||||
CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE)
|
CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE)
|
||||||
|
|
||||||
@ -96,3 +98,62 @@ class WindowCovering(HomeAccessory):
|
|||||||
abs(current_position - self.homekit_target) < 6:
|
abs(current_position - self.homekit_target) < 6:
|
||||||
self.char_target_position.set_value(current_position)
|
self.char_target_position.set_value(current_position)
|
||||||
self.homekit_target = None
|
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)
|
||||||
|
@ -154,6 +154,13 @@ class TestGetAccessories(unittest.TestCase):
|
|||||||
{ATTR_SUPPORTED_FEATURES: 4})
|
{ATTR_SUPPORTED_FEATURES: 4})
|
||||||
get_accessory(None, state, 2, {})
|
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):
|
def test_alarm_control_panel(self):
|
||||||
"""Test alarm control panel."""
|
"""Test alarm control panel."""
|
||||||
config = {ATTR_CODE: '1234'}
|
config = {ATTR_CODE: '1234'}
|
||||||
|
@ -3,12 +3,13 @@ import unittest
|
|||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.components.cover import (
|
from homeassistant.components.cover import (
|
||||||
ATTR_POSITION, ATTR_CURRENT_POSITION)
|
ATTR_POSITION, ATTR_CURRENT_POSITION, SUPPORT_STOP)
|
||||||
from homeassistant.components.homekit.type_covers import (
|
from homeassistant.components.homekit.type_covers import (
|
||||||
GarageDoorOpener, WindowCovering)
|
GarageDoorOpener, WindowCovering, WindowCoveringBasic)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_OPEN,
|
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
|
from tests.common import get_test_home_assistant
|
||||||
|
|
||||||
@ -132,9 +133,117 @@ class TestHomekitSensors(unittest.TestCase):
|
|||||||
acc.char_target_position.client_update_value(75)
|
acc.char_target_position.client_update_value(75)
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.events[0].data[ATTR_SERVICE], 'set_cover_position')
|
self.events[1].data[ATTR_SERVICE], 'set_cover_position')
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.events[1].data[ATTR_SERVICE_DATA][ATTR_POSITION], 75)
|
self.events[1].data[ATTR_SERVICE_DATA][ATTR_POSITION], 75)
|
||||||
|
|
||||||
self.assertEqual(acc.char_current_position.value, 50)
|
self.assertEqual(acc.char_current_position.value, 50)
|
||||||
self.assertEqual(acc.char_target_position.value, 75)
|
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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user