Add tilt support to basic homekit window covers (#33937)

* Add tilt support to basic homekit window covers

* Add stop support to all window covers

* protect supports_stop
pull/34123/head
J. Nick Koston 2020-04-12 17:27:17 -05:00 committed by GitHub
parent e6a6c3ceb6
commit c75d3ce8c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 109 additions and 54 deletions

View File

@ -148,6 +148,7 @@ CHAR_TARGET_HUMIDITY = "TargetRelativeHumidity"
CHAR_TARGET_SECURITY_STATE = "SecuritySystemTargetState"
CHAR_TARGET_TEMPERATURE = "TargetTemperature"
CHAR_TARGET_TILT_ANGLE = "TargetHorizontalTiltAngle"
CHAR_HOLD_POSITION = "HoldPosition"
CHAR_TEMP_DISPLAY_UNITS = "TemperatureDisplayUnits"
CHAR_VALVE_TYPE = "ValveType"
CHAR_VOLUME = "Volume"

View File

@ -32,6 +32,7 @@ from .const import (
CHAR_CURRENT_DOOR_STATE,
CHAR_CURRENT_POSITION,
CHAR_CURRENT_TILT_ANGLE,
CHAR_HOLD_POSITION,
CHAR_POSITION_STATE,
CHAR_TARGET_DOOR_STATE,
CHAR_TARGET_POSITION,
@ -123,47 +124,50 @@ class GarageDoorOpener(HomeAccessory):
self.char_current_state.set_value(current_door_state)
@TYPES.register("WindowCovering")
class WindowCovering(HomeAccessory):
"""Generate a Window accessory for a cover entity.
class WindowCoveringBase(HomeAccessory):
"""Generate a base Window accessory for a cover entity.
The cover entity must support: set_cover_position.
This class is used for WindowCoveringBasic and
WindowCovering
"""
def __init__(self, *args):
"""Initialize a WindowCovering accessory object."""
def __init__(self, *args, category):
"""Initialize a WindowCoveringBase accessory object."""
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
self._homekit_target = None
self._homekit_target_tilt = None
serv_cover = self.add_preload_service(
SERV_WINDOW_COVERING,
chars=[CHAR_TARGET_TILT_ANGLE, CHAR_CURRENT_TILT_ANGLE],
)
features = self.hass.states.get(self.entity_id).attributes.get(
self.features = self.hass.states.get(self.entity_id).attributes.get(
ATTR_SUPPORTED_FEATURES, 0
)
self._supports_stop = self.features & SUPPORT_STOP
self._homekit_target_tilt = None
self.chars = []
if self._supports_stop:
self.chars.append(CHAR_HOLD_POSITION)
self._supports_tilt = self.features & SUPPORT_SET_TILT_POSITION
self._supports_tilt = features & SUPPORT_SET_TILT_POSITION
if self._supports_tilt:
self.char_target_tilt = serv_cover.configure_char(
self.chars.extend([CHAR_TARGET_TILT_ANGLE, CHAR_CURRENT_TILT_ANGLE])
self.serv_cover = self.add_preload_service(SERV_WINDOW_COVERING, self.chars)
if self._supports_stop:
self.char_hold_position = self.serv_cover.configure_char(
CHAR_HOLD_POSITION, setter_callback=self.set_stop
)
if self._supports_tilt:
self.char_target_tilt = self.serv_cover.configure_char(
CHAR_TARGET_TILT_ANGLE, setter_callback=self.set_tilt
)
self.char_current_tilt = serv_cover.configure_char(
self.char_current_tilt = self.serv_cover.configure_char(
CHAR_CURRENT_TILT_ANGLE, value=0
)
self.char_current_position = serv_cover.configure_char(
CHAR_CURRENT_POSITION, value=0
)
self.char_target_position = serv_cover.configure_char(
CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover
)
self.char_position_state = serv_cover.configure_char(
CHAR_POSITION_STATE, value=2
)
def set_stop(self, value):
"""Stop the cover motion from HomeKit."""
if value != 1:
return
self.call_service(DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: self.entity_id})
@debounce
def set_tilt(self, value):
@ -179,6 +183,51 @@ class WindowCovering(HomeAccessory):
self.call_service(DOMAIN, SERVICE_SET_COVER_TILT_POSITION, params, value)
def update_state(self, new_state):
"""Update cover position and tilt after state changed."""
# update tilt
current_tilt = new_state.attributes.get(ATTR_CURRENT_TILT_POSITION)
if isinstance(current_tilt, (float, int)):
# HomeKit sends values between -90 and 90.
# We'll have to normalize to [0,100]
current_tilt = (current_tilt / 100.0 * 180.0) - 90.0
current_tilt = int(current_tilt)
self.char_current_tilt.set_value(current_tilt)
# We have to assume that the device has worse precision than HomeKit.
# If it reports back a state that is only _close_ to HK's requested
# state, we'll "fix" what HomeKit requested so that it won't appear
# out of sync.
if self._homekit_target_tilt is None or abs(
current_tilt - self._homekit_target_tilt < DEVICE_PRECISION_LEEWAY
):
self.char_target_tilt.set_value(current_tilt)
self._homekit_target_tilt = None
@TYPES.register("WindowCovering")
class WindowCovering(WindowCoveringBase, HomeAccessory):
"""Generate a Window accessory for a cover entity.
The cover entity must support: set_cover_position.
"""
def __init__(self, *args):
"""Initialize a WindowCovering accessory object."""
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
self._homekit_target = None
self.char_current_position = self.serv_cover.configure_char(
CHAR_CURRENT_POSITION, value=0
)
self.char_target_position = self.serv_cover.configure_char(
CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover
)
self.char_position_state = self.serv_cover.configure_char(
CHAR_POSITION_STATE, value=2
)
@debounce
def move_cover(self, value):
"""Move cover to value if call came from HomeKit."""
@ -213,28 +262,11 @@ class WindowCovering(HomeAccessory):
else:
self.char_position_state.set_value(2)
# update tilt
current_tilt = new_state.attributes.get(ATTR_CURRENT_TILT_POSITION)
if isinstance(current_tilt, (float, int)):
# HomeKit sends values between -90 and 90.
# We'll have to normalize to [0,100]
current_tilt = (current_tilt / 100.0 * 180.0) - 90.0
current_tilt = int(current_tilt)
self.char_current_tilt.set_value(current_tilt)
# We have to assume that the device has worse precision than HomeKit.
# If it reports back a state that is only _close_ to HK's requested
# state, we'll "fix" what HomeKit requested so that it won't appear
# out of sync.
if self._homekit_target_tilt is None or abs(
current_tilt - self._homekit_target_tilt < DEVICE_PRECISION_LEEWAY
):
self.char_target_tilt.set_value(current_tilt)
self._homekit_target_tilt = None
super().update_state(new_state)
@TYPES.register("WindowCoveringBasic")
class WindowCoveringBasic(HomeAccessory):
class WindowCoveringBasic(WindowCoveringBase, HomeAccessory):
"""Generate a Window accessory for a cover entity.
The cover entity must support: open_cover, close_cover,
@ -244,19 +276,14 @@ class WindowCoveringBasic(HomeAccessory):
def __init__(self, *args):
"""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 = self.add_preload_service(SERV_WINDOW_COVERING)
self.char_current_position = serv_cover.configure_char(
self.char_current_position = self.serv_cover.configure_char(
CHAR_CURRENT_POSITION, value=0
)
self.char_target_position = serv_cover.configure_char(
self.char_target_position = self.serv_cover.configure_char(
CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover
)
self.char_position_state = serv_cover.configure_char(
self.char_position_state = self.serv_cover.configure_char(
CHAR_POSITION_STATE, value=2
)
@ -298,3 +325,5 @@ class WindowCoveringBasic(HomeAccessory):
self.char_position_state.set_value(0)
else:
self.char_position_state.set_value(2)
super().update_state(new_state)

View File

@ -9,6 +9,7 @@ from homeassistant.components.cover import (
ATTR_POSITION,
ATTR_TILT_POSITION,
DOMAIN,
SUPPORT_SET_POSITION,
SUPPORT_SET_TILT_POSITION,
SUPPORT_STOP,
)
@ -395,6 +396,30 @@ async def test_window_open_close_stop(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] is None
async def test_window_open_close_with_position_and_stop(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = "cover.stop_window"
hass.states.async_set(
entity_id,
STATE_UNKNOWN,
{ATTR_SUPPORTED_FEATURES: SUPPORT_STOP | SUPPORT_SET_POSITION},
)
acc = cls.window(hass, hk_driver, "Cover", entity_id, 2, None)
await acc.run_handler()
# Set from HomeKit
call_stop_cover = async_mock_service(hass, DOMAIN, "stop_cover")
await hass.async_add_executor_job(acc.char_hold_position.client_update_value, 1)
await hass.async_block_till_done()
assert call_stop_cover
assert call_stop_cover[0].data[ATTR_ENTITY_ID] == entity_id
assert acc.char_hold_position.value == 1
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] is None
async def test_window_basic_restore(hass, hk_driver, cls, events):
"""Test setting up an entity from state in the event registry."""
hass.state = CoreState.not_running