Add support for homekit windows (#40635)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
2b00d28af9
commit
a19b43a304
5 changed files with 106 additions and 34 deletions
|
@ -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):
|
||||
|
|
|
@ -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 ####
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue