Add support for homekit windows (#40635)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Julius Mittenzwei 2020-09-27 07:07:59 +02:00 committed by GitHub
parent 2b00d28af9
commit a19b43a304
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 106 additions and 34 deletions

View file

@ -9,7 +9,11 @@ from pyhap.accessory_driver import AccessoryDriver
from pyhap.const import CATEGORY_OTHER
from homeassistant.components import cover, vacuum
from homeassistant.components.cover import DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE
from homeassistant.components.cover import (
DEVICE_CLASS_GARAGE,
DEVICE_CLASS_GATE,
DEVICE_CLASS_WINDOW,
)
from homeassistant.components.media_player import DEVICE_CLASS_TV
from homeassistant.const import (
ATTR_BATTERY_CHARGING,
@ -155,6 +159,11 @@ def get_accessory(hass, driver, state, aid, config):
cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE
):
a_type = "GarageDoorOpener"
elif (
device_class == DEVICE_CLASS_WINDOW
and features & cover.SUPPORT_SET_POSITION
):
a_type = "Window"
elif features & cover.SUPPORT_SET_POSITION:
a_type = "WindowCovering"
elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE):

View file

@ -136,6 +136,7 @@ SERV_TELEVISION_SPEAKER = "TelevisionSpeaker"
SERV_TEMPERATURE_SENSOR = "TemperatureSensor"
SERV_THERMOSTAT = "Thermostat"
SERV_VALVE = "Valve"
SERV_WINDOW = "Window"
SERV_WINDOW_COVERING = "WindowCovering"
# #### Characteristics ####

View file

@ -1,7 +1,11 @@
"""Class to hold all cover accessories."""
import logging
from pyhap.const import CATEGORY_GARAGE_DOOR_OPENER, CATEGORY_WINDOW_COVERING
from pyhap.const import (
CATEGORY_GARAGE_DOOR_OPENER,
CATEGORY_WINDOW,
CATEGORY_WINDOW_COVERING,
)
from homeassistant.components.cover import (
ATTR_CURRENT_POSITION,
@ -46,6 +50,7 @@ from .const import (
HK_POSITION_GOING_TO_MIN,
HK_POSITION_STOPPED,
SERV_GARAGE_DOOR_OPENER,
SERV_WINDOW,
SERV_WINDOW_COVERING,
)
@ -128,16 +133,16 @@ class GarageDoorOpener(HomeAccessory):
self.char_current_state.set_value(current_door_state)
class WindowCoveringBase(HomeAccessory):
class OpeningDeviceBase(HomeAccessory):
"""Generate a base Window accessory for a cover entity.
This class is used for WindowCoveringBasic and
WindowCovering
"""
def __init__(self, *args, category):
"""Initialize a WindowCoveringBase accessory object."""
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
def __init__(self, *args, category, service):
"""Initialize a OpeningDeviceBase accessory object."""
super().__init__(*args, category=category)
state = self.hass.states.get(self.entity_id)
self.features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
@ -151,7 +156,7 @@ class WindowCoveringBase(HomeAccessory):
if self._supports_tilt:
self.chars.extend([CHAR_TARGET_TILT_ANGLE, CHAR_CURRENT_TILT_ANGLE])
self.serv_cover = self.add_preload_service(SERV_WINDOW_COVERING, self.chars)
self.serv_cover = self.add_preload_service(service, self.chars)
if self._supports_stop:
self.char_hold_position = self.serv_cover.configure_char(
@ -211,16 +216,15 @@ class WindowCoveringBase(HomeAccessory):
self._homekit_target_tilt = None
@TYPES.register("WindowCovering")
class WindowCovering(WindowCoveringBase, HomeAccessory):
"""Generate a Window accessory for a cover entity.
class OpeningDevice(OpeningDeviceBase, HomeAccessory):
"""Generate a Window/WindowOpening accessory for a cover entity.
The cover entity must support: set_cover_position.
"""
def __init__(self, *args):
def __init__(self, *args, category, service):
"""Initialize a WindowCovering accessory object."""
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
super().__init__(*args, category=category, service=service)
state = self.hass.states.get(self.entity_id)
self._homekit_target = None
@ -278,8 +282,34 @@ class WindowCovering(WindowCoveringBase, HomeAccessory):
super().async_update_state(new_state)
@TYPES.register("Window")
class Window(OpeningDevice):
"""Generate a Window accessory for a cover entity with DEVICE_CLASS_WINDOW.
The entity must support: set_cover_position.
"""
def __init__(self, *args):
"""Initialize a Window accessory object."""
super().__init__(*args, category=CATEGORY_WINDOW, service=SERV_WINDOW)
@TYPES.register("WindowCovering")
class WindowCovering(OpeningDevice):
"""Generate a WindowCovering accessory for a cover entity.
The entity must support: set_cover_position.
"""
def __init__(self, *args):
"""Initialize a WindowCovering accessory object."""
super().__init__(
*args, category=CATEGORY_WINDOW_COVERING, service=SERV_WINDOW_COVERING
)
@TYPES.register("WindowCoveringBasic")
class WindowCoveringBasic(WindowCoveringBase, HomeAccessory):
class WindowCoveringBasic(OpeningDeviceBase, HomeAccessory):
"""Generate a Window accessory for a cover entity.
The cover entity must support: open_cover, close_cover,
@ -287,8 +317,10 @@ class WindowCoveringBasic(WindowCoveringBase, HomeAccessory):
"""
def __init__(self, *args):
"""Initialize a WindowCovering accessory object."""
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
"""Initialize a WindowCoveringBasic accessory object."""
super().__init__(
*args, category=CATEGORY_WINDOW_COVERING, service=SERV_WINDOW_COVERING
)
state = self.hass.states.get(self.entity_id)
self.char_current_position = self.serv_cover.configure_char(
CHAR_CURRENT_POSITION, value=0

View file

@ -120,6 +120,12 @@ def test_types(type_name, entity_id, state, attrs, config):
ATTR_SUPPORTED_FEATURES: cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE,
},
),
(
"Window",
"cover.set_position",
"open",
{ATTR_DEVICE_CLASS: "window", ATTR_SUPPORTED_FEATURES: 4},
),
("WindowCovering", "cover.set_position", "open", {ATTR_SUPPORTED_FEATURES: 4}),
(
"WindowCoveringBasic",

View file

@ -48,10 +48,13 @@ def cls():
"homeassistant.components.homekit.type_covers",
fromlist=["GarageDoorOpener", "WindowCovering", "WindowCoveringBasic"],
)
patcher_tuple = namedtuple("Cls", ["window", "window_basic", "garage"])
patcher_tuple = namedtuple(
"Cls", ["window", "windowcovering", "windowcovering_basic", "garage"]
)
yield patcher_tuple(
window=_import.WindowCovering,
window_basic=_import.WindowCoveringBasic,
window=_import.Window,
windowcovering=_import.WindowCovering,
windowcovering_basic=_import.WindowCoveringBasic,
garage=_import.GarageDoorOpener,
)
patcher.stop()
@ -136,13 +139,13 @@ async def test_garage_door_open_close(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] is None
async def test_window_set_cover_position(hass, hk_driver, cls, events):
async def test_windowcovering_set_cover_position(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = "cover.window"
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
acc = cls.window(hass, hk_driver, "Cover", entity_id, 2, None)
acc = cls.windowcovering(hass, hk_driver, "Cover", entity_id, 2, None)
await acc.run_handler()
await hass.async_block_till_done()
@ -206,7 +209,24 @@ async def test_window_set_cover_position(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] == 75
async def test_window_cover_set_tilt(hass, hk_driver, cls, events):
async def test_window_instantiate(hass, hk_driver, cls, events):
"""Test if Window accessory is instantiated correctly."""
entity_id = "cover.window"
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
acc = cls.window(hass, hk_driver, "Window", entity_id, 2, None)
await acc.run_handler()
await hass.async_block_till_done()
assert acc.aid == 2
assert acc.category == 13 # Window
assert acc.char_current_position.value == 0
assert acc.char_target_position.value == 0
async def test_windowcovering_cover_set_tilt(hass, hk_driver, cls, events):
"""Test if accessory and HA update slat tilt accordingly."""
entity_id = "cover.window"
@ -214,7 +234,7 @@ async def test_window_cover_set_tilt(hass, hk_driver, cls, events):
entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_TILT_POSITION}
)
await hass.async_block_till_done()
acc = cls.window(hass, hk_driver, "Cover", entity_id, 2, None)
acc = cls.windowcovering(hass, hk_driver, "Cover", entity_id, 2, None)
await acc.run_handler()
await hass.async_block_till_done()
@ -273,12 +293,12 @@ async def test_window_cover_set_tilt(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] == 75
async def test_window_open_close(hass, hk_driver, cls, events):
async def test_windowcovering_open_close(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = "cover.window"
hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: 0})
acc = cls.window_basic(hass, hk_driver, "Cover", entity_id, 2, None)
acc = cls.windowcovering_basic(hass, hk_driver, "Cover", entity_id, 2, None)
await acc.run_handler()
await hass.async_block_till_done()
@ -354,14 +374,14 @@ async def test_window_open_close(hass, hk_driver, cls, events):
assert events[-1].data[ATTR_VALUE] is None
async def test_window_open_close_stop(hass, hk_driver, cls, events):
async def test_windowcovering_open_close_stop(hass, hk_driver, cls, events):
"""Test if accessory and HA are updated accordingly."""
entity_id = "cover.window"
hass.states.async_set(
entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}
)
acc = cls.window_basic(hass, hk_driver, "Cover", entity_id, 2, None)
acc = cls.windowcovering_basic(hass, hk_driver, "Cover", entity_id, 2, None)
await acc.run_handler()
await hass.async_block_till_done()
@ -401,7 +421,9 @@ 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):
async def test_windowcovering_open_close_with_position_and_stop(
hass, hk_driver, cls, events
):
"""Test if accessory and HA are updated accordingly."""
entity_id = "cover.stop_window"
@ -410,7 +432,7 @@ async def test_window_open_close_with_position_and_stop(hass, hk_driver, cls, ev
STATE_UNKNOWN,
{ATTR_SUPPORTED_FEATURES: SUPPORT_STOP | SUPPORT_SET_POSITION},
)
acc = cls.window(hass, hk_driver, "Cover", entity_id, 2, None)
acc = cls.windowcovering(hass, hk_driver, "Cover", entity_id, 2, None)
await acc.run_handler()
await hass.async_block_till_done()
@ -430,7 +452,7 @@ async def test_window_open_close_with_position_and_stop(hass, hk_driver, cls, ev
assert events[-1].data[ATTR_VALUE] is None
async def test_window_basic_restore(hass, hk_driver, cls, events):
async def test_windowcovering_basic_restore(hass, hk_driver, cls, events):
"""Test setting up an entity from state in the event registry."""
hass.state = CoreState.not_running
@ -455,20 +477,22 @@ async def test_window_basic_restore(hass, hk_driver, cls, events):
hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {})
await hass.async_block_till_done()
acc = cls.window_basic(hass, hk_driver, "Cover", "cover.simple", 2, None)
acc = cls.windowcovering_basic(hass, hk_driver, "Cover", "cover.simple", 2, None)
assert acc.category == 14
assert acc.char_current_position is not None
assert acc.char_target_position is not None
assert acc.char_position_state is not None
acc = cls.window_basic(hass, hk_driver, "Cover", "cover.all_info_set", 2, None)
acc = cls.windowcovering_basic(
hass, hk_driver, "Cover", "cover.all_info_set", 2, None
)
assert acc.category == 14
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_window_restore(hass, hk_driver, cls, events):
async def test_windowcovering_restore(hass, hk_driver, cls, events):
"""Test setting up an entity from state in the event registry."""
hass.state = CoreState.not_running
@ -493,13 +517,13 @@ async def test_window_restore(hass, hk_driver, cls, events):
hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {})
await hass.async_block_till_done()
acc = cls.window(hass, hk_driver, "Cover", "cover.simple", 2, None)
acc = cls.windowcovering(hass, hk_driver, "Cover", "cover.simple", 2, None)
assert acc.category == 14
assert acc.char_current_position is not None
assert acc.char_target_position is not None
assert acc.char_position_state is not None
acc = cls.window(hass, hk_driver, "Cover", "cover.all_info_set", 2, None)
acc = cls.windowcovering(hass, hk_driver, "Cover", "cover.all_info_set", 2, None)
assert acc.category == 14
assert acc.char_current_position is not None
assert acc.char_target_position is not None