Add support for `linked_doorbell_sensor` to HomeKit locks (#131660)
Co-authored-by: J. Nick Koston <nick@koston.org>pull/131973/head
parent
6da2515d7a
commit
bcdac7ed37
|
@ -33,6 +33,7 @@ from homeassistant.components.device_automation.trigger import (
|
||||||
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN, EventDeviceClass
|
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN, EventDeviceClass
|
||||||
from homeassistant.components.http import KEY_HASS, HomeAssistantView
|
from homeassistant.components.http import KEY_HASS, HomeAssistantView
|
||||||
from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN
|
from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN
|
||||||
|
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
|
||||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
@ -1133,6 +1134,8 @@ class HomeKit:
|
||||||
config[entity_id].setdefault(
|
config[entity_id].setdefault(
|
||||||
CONF_LINKED_MOTION_SENSOR, motion_binary_sensor_entity_id
|
CONF_LINKED_MOTION_SENSOR, motion_binary_sensor_entity_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if domain in (CAMERA_DOMAIN, LOCK_DOMAIN):
|
||||||
if doorbell_event_entity_id := lookup.get(DOORBELL_EVENT_SENSOR):
|
if doorbell_event_entity_id := lookup.get(DOORBELL_EVENT_SENSOR):
|
||||||
config[entity_id].setdefault(
|
config[entity_id].setdefault(
|
||||||
CONF_LINKED_DOORBELL_SENSOR, doorbell_event_entity_id
|
CONF_LINKED_DOORBELL_SENSOR, doorbell_event_entity_id
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
"""Extend the doorbell functions."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pyhap.util import callback as pyhap_callback
|
||||||
|
|
||||||
|
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||||
|
from homeassistant.core import (
|
||||||
|
Event,
|
||||||
|
EventStateChangedData,
|
||||||
|
HassJobType,
|
||||||
|
State,
|
||||||
|
callback as ha_callback,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.event import async_track_state_change_event
|
||||||
|
|
||||||
|
from .accessories import HomeAccessory
|
||||||
|
from .const import (
|
||||||
|
CHAR_MUTE,
|
||||||
|
CHAR_PROGRAMMABLE_SWITCH_EVENT,
|
||||||
|
CONF_LINKED_DOORBELL_SENSOR,
|
||||||
|
SERV_DOORBELL,
|
||||||
|
SERV_SPEAKER,
|
||||||
|
SERV_STATELESS_PROGRAMMABLE_SWITCH,
|
||||||
|
)
|
||||||
|
from .util import state_changed_event_is_same_state
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DOORBELL_SINGLE_PRESS = 0
|
||||||
|
DOORBELL_DOUBLE_PRESS = 1
|
||||||
|
DOORBELL_LONG_PRESS = 2
|
||||||
|
|
||||||
|
|
||||||
|
class HomeDoorbellAccessory(HomeAccessory):
|
||||||
|
"""Accessory with optional doorbell."""
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""Initialize an Accessory object with optional attached doorbell."""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._char_doorbell_detected = None
|
||||||
|
self._char_doorbell_detected_switch = None
|
||||||
|
linked_doorbell_sensor: str | None
|
||||||
|
linked_doorbell_sensor = self.config.get(CONF_LINKED_DOORBELL_SENSOR)
|
||||||
|
self.linked_doorbell_sensor = linked_doorbell_sensor
|
||||||
|
self.doorbell_is_event = False
|
||||||
|
if not linked_doorbell_sensor:
|
||||||
|
return
|
||||||
|
self.doorbell_is_event = linked_doorbell_sensor.startswith("event.")
|
||||||
|
if not (state := self.hass.states.get(linked_doorbell_sensor)):
|
||||||
|
return
|
||||||
|
serv_doorbell = self.add_preload_service(SERV_DOORBELL)
|
||||||
|
self.set_primary_service(serv_doorbell)
|
||||||
|
self._char_doorbell_detected = serv_doorbell.configure_char(
|
||||||
|
CHAR_PROGRAMMABLE_SWITCH_EVENT,
|
||||||
|
value=0,
|
||||||
|
)
|
||||||
|
serv_stateless_switch = self.add_preload_service(
|
||||||
|
SERV_STATELESS_PROGRAMMABLE_SWITCH
|
||||||
|
)
|
||||||
|
self._char_doorbell_detected_switch = serv_stateless_switch.configure_char(
|
||||||
|
CHAR_PROGRAMMABLE_SWITCH_EVENT,
|
||||||
|
value=0,
|
||||||
|
valid_values={"SinglePress": DOORBELL_SINGLE_PRESS},
|
||||||
|
)
|
||||||
|
serv_speaker = self.add_preload_service(SERV_SPEAKER)
|
||||||
|
serv_speaker.configure_char(CHAR_MUTE, value=0)
|
||||||
|
self.async_update_doorbell_state(None, state)
|
||||||
|
|
||||||
|
@ha_callback
|
||||||
|
@pyhap_callback # type: ignore[misc]
|
||||||
|
def run(self) -> None:
|
||||||
|
"""Handle doorbell event."""
|
||||||
|
if self._char_doorbell_detected:
|
||||||
|
assert self.linked_doorbell_sensor
|
||||||
|
self._subscriptions.append(
|
||||||
|
async_track_state_change_event(
|
||||||
|
self.hass,
|
||||||
|
self.linked_doorbell_sensor,
|
||||||
|
self.async_update_doorbell_state_event,
|
||||||
|
job_type=HassJobType.Callback,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
super().run()
|
||||||
|
|
||||||
|
@ha_callback
|
||||||
|
def async_update_doorbell_state_event(
|
||||||
|
self, event: Event[EventStateChangedData]
|
||||||
|
) -> None:
|
||||||
|
"""Handle state change event listener callback."""
|
||||||
|
if not state_changed_event_is_same_state(event) and (
|
||||||
|
new_state := event.data["new_state"]
|
||||||
|
):
|
||||||
|
self.async_update_doorbell_state(event.data["old_state"], new_state)
|
||||||
|
|
||||||
|
@ha_callback
|
||||||
|
def async_update_doorbell_state(
|
||||||
|
self, old_state: State | None, new_state: State
|
||||||
|
) -> None:
|
||||||
|
"""Handle link doorbell sensor state change to update HomeKit value."""
|
||||||
|
assert self._char_doorbell_detected
|
||||||
|
assert self._char_doorbell_detected_switch
|
||||||
|
state = new_state.state
|
||||||
|
if state == STATE_ON or (
|
||||||
|
self.doorbell_is_event
|
||||||
|
and old_state is not None
|
||||||
|
and old_state.state != STATE_UNAVAILABLE
|
||||||
|
and state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||||
|
):
|
||||||
|
self._char_doorbell_detected.set_value(DOORBELL_SINGLE_PRESS)
|
||||||
|
self._char_doorbell_detected_switch.set_value(DOORBELL_SINGLE_PRESS)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"%s: Set linked doorbell %s sensor to %d",
|
||||||
|
self.entity_id,
|
||||||
|
self.linked_doorbell_sensor,
|
||||||
|
DOORBELL_SINGLE_PRESS,
|
||||||
|
)
|
|
@ -31,15 +31,12 @@ from homeassistant.helpers.event import (
|
||||||
)
|
)
|
||||||
from homeassistant.util.async_ import create_eager_task
|
from homeassistant.util.async_ import create_eager_task
|
||||||
|
|
||||||
from .accessories import TYPES, HomeAccessory, HomeDriver
|
from .accessories import TYPES, HomeDriver
|
||||||
from .const import (
|
from .const import (
|
||||||
CHAR_MOTION_DETECTED,
|
CHAR_MOTION_DETECTED,
|
||||||
CHAR_MUTE,
|
|
||||||
CHAR_PROGRAMMABLE_SWITCH_EVENT,
|
|
||||||
CONF_AUDIO_CODEC,
|
CONF_AUDIO_CODEC,
|
||||||
CONF_AUDIO_MAP,
|
CONF_AUDIO_MAP,
|
||||||
CONF_AUDIO_PACKET_SIZE,
|
CONF_AUDIO_PACKET_SIZE,
|
||||||
CONF_LINKED_DOORBELL_SENSOR,
|
|
||||||
CONF_LINKED_MOTION_SENSOR,
|
CONF_LINKED_MOTION_SENSOR,
|
||||||
CONF_MAX_FPS,
|
CONF_MAX_FPS,
|
||||||
CONF_MAX_HEIGHT,
|
CONF_MAX_HEIGHT,
|
||||||
|
@ -64,18 +61,13 @@ from .const import (
|
||||||
DEFAULT_VIDEO_MAP,
|
DEFAULT_VIDEO_MAP,
|
||||||
DEFAULT_VIDEO_PACKET_SIZE,
|
DEFAULT_VIDEO_PACKET_SIZE,
|
||||||
DEFAULT_VIDEO_PROFILE_NAMES,
|
DEFAULT_VIDEO_PROFILE_NAMES,
|
||||||
SERV_DOORBELL,
|
|
||||||
SERV_MOTION_SENSOR,
|
SERV_MOTION_SENSOR,
|
||||||
SERV_SPEAKER,
|
|
||||||
SERV_STATELESS_PROGRAMMABLE_SWITCH,
|
|
||||||
)
|
)
|
||||||
|
from .doorbell import HomeDoorbellAccessory
|
||||||
from .util import pid_is_alive, state_changed_event_is_same_state
|
from .util import pid_is_alive, state_changed_event_is_same_state
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOORBELL_SINGLE_PRESS = 0
|
|
||||||
DOORBELL_DOUBLE_PRESS = 1
|
|
||||||
DOORBELL_LONG_PRESS = 2
|
|
||||||
|
|
||||||
VIDEO_OUTPUT = (
|
VIDEO_OUTPUT = (
|
||||||
"-map {v_map} -an "
|
"-map {v_map} -an "
|
||||||
|
@ -149,7 +141,7 @@ CONFIG_DEFAULTS = {
|
||||||
@TYPES.register("Camera")
|
@TYPES.register("Camera")
|
||||||
# False-positive on pylint, not a CameraEntity
|
# False-positive on pylint, not a CameraEntity
|
||||||
# pylint: disable-next=hass-enforce-class-module
|
# pylint: disable-next=hass-enforce-class-module
|
||||||
class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc]
|
class Camera(HomeDoorbellAccessory, PyhapCamera): # type: ignore[misc]
|
||||||
"""Generate a Camera accessory."""
|
"""Generate a Camera accessory."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -237,36 +229,6 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc]
|
||||||
)
|
)
|
||||||
self._async_update_motion_state(None, state)
|
self._async_update_motion_state(None, state)
|
||||||
|
|
||||||
self._char_doorbell_detected = None
|
|
||||||
self._char_doorbell_detected_switch = None
|
|
||||||
linked_doorbell_sensor: str | None = self.config.get(
|
|
||||||
CONF_LINKED_DOORBELL_SENSOR
|
|
||||||
)
|
|
||||||
self.linked_doorbell_sensor = linked_doorbell_sensor
|
|
||||||
self.doorbell_is_event = False
|
|
||||||
if not linked_doorbell_sensor:
|
|
||||||
return
|
|
||||||
self.doorbell_is_event = linked_doorbell_sensor.startswith("event.")
|
|
||||||
if not (state := self.hass.states.get(linked_doorbell_sensor)):
|
|
||||||
return
|
|
||||||
serv_doorbell = self.add_preload_service(SERV_DOORBELL)
|
|
||||||
self.set_primary_service(serv_doorbell)
|
|
||||||
self._char_doorbell_detected = serv_doorbell.configure_char(
|
|
||||||
CHAR_PROGRAMMABLE_SWITCH_EVENT,
|
|
||||||
value=0,
|
|
||||||
)
|
|
||||||
serv_stateless_switch = self.add_preload_service(
|
|
||||||
SERV_STATELESS_PROGRAMMABLE_SWITCH
|
|
||||||
)
|
|
||||||
self._char_doorbell_detected_switch = serv_stateless_switch.configure_char(
|
|
||||||
CHAR_PROGRAMMABLE_SWITCH_EVENT,
|
|
||||||
value=0,
|
|
||||||
valid_values={"SinglePress": DOORBELL_SINGLE_PRESS},
|
|
||||||
)
|
|
||||||
serv_speaker = self.add_preload_service(SERV_SPEAKER)
|
|
||||||
serv_speaker.configure_char(CHAR_MUTE, value=0)
|
|
||||||
self._async_update_doorbell_state(None, state)
|
|
||||||
|
|
||||||
@pyhap_callback # type: ignore[misc]
|
@pyhap_callback # type: ignore[misc]
|
||||||
@callback
|
@callback
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
|
@ -285,17 +247,6 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._char_doorbell_detected:
|
|
||||||
assert self.linked_doorbell_sensor
|
|
||||||
self._subscriptions.append(
|
|
||||||
async_track_state_change_event(
|
|
||||||
self.hass,
|
|
||||||
self.linked_doorbell_sensor,
|
|
||||||
self._async_update_doorbell_state_event,
|
|
||||||
job_type=HassJobType.Callback,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
super().run()
|
super().run()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -344,39 +295,6 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc]
|
||||||
detected,
|
detected,
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_update_doorbell_state_event(
|
|
||||||
self, event: Event[EventStateChangedData]
|
|
||||||
) -> None:
|
|
||||||
"""Handle state change event listener callback."""
|
|
||||||
if not state_changed_event_is_same_state(event) and (
|
|
||||||
new_state := event.data["new_state"]
|
|
||||||
):
|
|
||||||
self._async_update_doorbell_state(event.data["old_state"], new_state)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_update_doorbell_state(
|
|
||||||
self, old_state: State | None, new_state: State
|
|
||||||
) -> None:
|
|
||||||
"""Handle link doorbell sensor state change to update HomeKit value."""
|
|
||||||
assert self._char_doorbell_detected
|
|
||||||
assert self._char_doorbell_detected_switch
|
|
||||||
state = new_state.state
|
|
||||||
if state == STATE_ON or (
|
|
||||||
self.doorbell_is_event
|
|
||||||
and old_state is not None
|
|
||||||
and old_state.state != STATE_UNAVAILABLE
|
|
||||||
and state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
|
||||||
):
|
|
||||||
self._char_doorbell_detected.set_value(DOORBELL_SINGLE_PRESS)
|
|
||||||
self._char_doorbell_detected_switch.set_value(DOORBELL_SINGLE_PRESS)
|
|
||||||
_LOGGER.debug(
|
|
||||||
"%s: Set linked doorbell %s sensor to %d",
|
|
||||||
self.entity_id,
|
|
||||||
self.linked_doorbell_sensor,
|
|
||||||
DOORBELL_SINGLE_PRESS,
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_update_state(self, new_state: State | None) -> None:
|
def async_update_state(self, new_state: State | None) -> None:
|
||||||
"""Handle state change to update HomeKit value."""
|
"""Handle state change to update HomeKit value."""
|
||||||
|
|
|
@ -9,8 +9,9 @@ from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockState
|
||||||
from homeassistant.const import ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN
|
from homeassistant.const import ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN
|
||||||
from homeassistant.core import State, callback
|
from homeassistant.core import State, callback
|
||||||
|
|
||||||
from .accessories import TYPES, HomeAccessory
|
from .accessories import TYPES
|
||||||
from .const import CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE, SERV_LOCK
|
from .const import CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE, SERV_LOCK
|
||||||
|
from .doorbell import HomeDoorbellAccessory
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -53,7 +54,7 @@ STATE_TO_SERVICE = {
|
||||||
|
|
||||||
|
|
||||||
@TYPES.register("Lock")
|
@TYPES.register("Lock")
|
||||||
class Lock(HomeAccessory):
|
class Lock(HomeDoorbellAccessory):
|
||||||
"""Generate a Lock accessory for a lock entity.
|
"""Generate a Lock accessory for a lock entity.
|
||||||
|
|
||||||
The lock entity must support: unlock and lock.
|
The lock entity must support: unlock and lock.
|
||||||
|
|
|
@ -182,7 +182,6 @@ HUMIDIFIER_SCHEMA = BASIC_INFO_SCHEMA.extend(
|
||||||
{vol.Optional(CONF_LINKED_HUMIDITY_SENSOR): cv.entity_domain(sensor.DOMAIN)}
|
{vol.Optional(CONF_LINKED_HUMIDITY_SENSOR): cv.entity_domain(sensor.DOMAIN)}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
COVER_SCHEMA = BASIC_INFO_SCHEMA.extend(
|
COVER_SCHEMA = BASIC_INFO_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Optional(CONF_LINKED_OBSTRUCTION_SENSOR): cv.entity_domain(
|
vol.Optional(CONF_LINKED_OBSTRUCTION_SENSOR): cv.entity_domain(
|
||||||
|
@ -195,6 +194,14 @@ CODE_SCHEMA = BASIC_INFO_SCHEMA.extend(
|
||||||
{vol.Optional(ATTR_CODE, default=None): vol.Any(None, cv.string)}
|
{vol.Optional(ATTR_CODE, default=None): vol.Any(None, cv.string)}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
LOCK_SCHEMA = CODE_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
vol.Optional(CONF_LINKED_DOORBELL_SENSOR): cv.entity_domain(
|
||||||
|
[binary_sensor.DOMAIN, EVENT_DOMAIN]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
MEDIA_PLAYER_SCHEMA = vol.Schema(
|
MEDIA_PLAYER_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_FEATURE): vol.All(
|
vol.Required(CONF_FEATURE): vol.All(
|
||||||
|
@ -284,7 +291,7 @@ def validate_entity_config(values: dict) -> dict[str, dict]:
|
||||||
if not isinstance(config, dict):
|
if not isinstance(config, dict):
|
||||||
raise vol.Invalid(f"The configuration for {entity} must be a dictionary.")
|
raise vol.Invalid(f"The configuration for {entity} must be a dictionary.")
|
||||||
|
|
||||||
if domain in ("alarm_control_panel", "lock"):
|
if domain == "alarm_control_panel":
|
||||||
config = CODE_SCHEMA(config)
|
config = CODE_SCHEMA(config)
|
||||||
|
|
||||||
elif domain == media_player.const.DOMAIN:
|
elif domain == media_player.const.DOMAIN:
|
||||||
|
@ -301,6 +308,9 @@ def validate_entity_config(values: dict) -> dict[str, dict]:
|
||||||
elif domain == "camera":
|
elif domain == "camera":
|
||||||
config = CAMERA_SCHEMA(config)
|
config = CAMERA_SCHEMA(config)
|
||||||
|
|
||||||
|
elif domain == "lock":
|
||||||
|
config = LOCK_SCHEMA(config)
|
||||||
|
|
||||||
elif domain == "switch":
|
elif domain == "switch":
|
||||||
config = SWITCH_TYPE_SCHEMA(config)
|
config = SWITCH_TYPE_SCHEMA(config)
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,34 @@
|
||||||
"""Test different accessory types: Locks."""
|
"""Test different accessory types: Locks."""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.homekit.const import ATTR_VALUE
|
from homeassistant.components import lock
|
||||||
|
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||||
|
from homeassistant.components.event import EventDeviceClass
|
||||||
|
from homeassistant.components.homekit.accessories import HomeBridge
|
||||||
|
from homeassistant.components.homekit.const import (
|
||||||
|
ATTR_VALUE,
|
||||||
|
CHAR_PROGRAMMABLE_SWITCH_EVENT,
|
||||||
|
CONF_LINKED_DOORBELL_SENSOR,
|
||||||
|
SERV_DOORBELL,
|
||||||
|
SERV_STATELESS_PROGRAMMABLE_SWITCH,
|
||||||
|
)
|
||||||
from homeassistant.components.homekit.type_locks import Lock
|
from homeassistant.components.homekit.type_locks import Lock
|
||||||
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockState
|
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockState
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_CODE,
|
ATTR_CODE,
|
||||||
|
ATTR_DEVICE_CLASS,
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_ON,
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
)
|
)
|
||||||
from homeassistant.core import Event, HomeAssistant
|
from homeassistant.core import Event, HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from tests.common import async_mock_service
|
from tests.common import async_mock_service
|
||||||
|
|
||||||
|
@ -135,3 +152,285 @@ async def test_no_code(
|
||||||
assert acc.char_target_state.value == 1
|
assert acc.char_target_state.value == 1
|
||||||
assert len(events) == 1
|
assert len(events) == 1
|
||||||
assert events[-1].data[ATTR_VALUE] is None
|
assert events[-1].data[ATTR_VALUE] is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_lock_with_linked_doorbell_sensor(hass: HomeAssistant, hk_driver) -> None:
|
||||||
|
"""Test a lock with a linked doorbell sensor can update."""
|
||||||
|
code = "1234"
|
||||||
|
await async_setup_component(hass, lock.DOMAIN, {lock.DOMAIN: {"platform": "demo"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
doorbell_entity_id = "binary_sensor.doorbell"
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
doorbell_entity_id,
|
||||||
|
STATE_ON,
|
||||||
|
{ATTR_DEVICE_CLASS: BinarySensorDeviceClass.OCCUPANCY},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
entity_id = "lock.demo_lock"
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, None)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc = Lock(
|
||||||
|
hass,
|
||||||
|
hk_driver,
|
||||||
|
"Lock",
|
||||||
|
entity_id,
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
ATTR_CODE: code,
|
||||||
|
CONF_LINKED_DOORBELL_SENSOR: doorbell_entity_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
bridge = HomeBridge("hass", hk_driver, "Test Bridge")
|
||||||
|
bridge.add_accessory(acc)
|
||||||
|
|
||||||
|
acc.run()
|
||||||
|
|
||||||
|
assert acc.aid == 2
|
||||||
|
assert acc.category == 6 # DoorLock
|
||||||
|
|
||||||
|
service = acc.get_service(SERV_DOORBELL)
|
||||||
|
assert service
|
||||||
|
char = service.get_characteristic(CHAR_PROGRAMMABLE_SWITCH_EVENT)
|
||||||
|
assert char
|
||||||
|
|
||||||
|
assert char.value is None
|
||||||
|
|
||||||
|
service2 = acc.get_service(SERV_STATELESS_PROGRAMMABLE_SWITCH)
|
||||||
|
assert service2
|
||||||
|
char2 = service.get_characteristic(CHAR_PROGRAMMABLE_SWITCH_EVENT)
|
||||||
|
assert char2
|
||||||
|
broker = MagicMock()
|
||||||
|
char2.broker = broker
|
||||||
|
assert char2.value is None
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
doorbell_entity_id,
|
||||||
|
STATE_OFF,
|
||||||
|
{ATTR_DEVICE_CLASS: BinarySensorDeviceClass.OCCUPANCY},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert char.value is None
|
||||||
|
assert char2.value is None
|
||||||
|
assert len(broker.mock_calls) == 0
|
||||||
|
|
||||||
|
char.set_value(True)
|
||||||
|
char2.set_value(True)
|
||||||
|
broker.reset_mock()
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
doorbell_entity_id,
|
||||||
|
STATE_ON,
|
||||||
|
{ATTR_DEVICE_CLASS: BinarySensorDeviceClass.OCCUPANCY},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert char.value is None
|
||||||
|
assert char2.value is None
|
||||||
|
assert len(broker.mock_calls) == 2
|
||||||
|
broker.reset_mock()
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
doorbell_entity_id,
|
||||||
|
STATE_ON,
|
||||||
|
{ATTR_DEVICE_CLASS: BinarySensorDeviceClass.OCCUPANCY},
|
||||||
|
force_update=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert char.value is None
|
||||||
|
assert char2.value is None
|
||||||
|
assert len(broker.mock_calls) == 0
|
||||||
|
broker.reset_mock()
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
doorbell_entity_id,
|
||||||
|
STATE_ON,
|
||||||
|
{ATTR_DEVICE_CLASS: BinarySensorDeviceClass.OCCUPANCY, "other": "attr"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert char.value is None
|
||||||
|
assert char2.value is None
|
||||||
|
assert len(broker.mock_calls) == 0
|
||||||
|
broker.reset_mock()
|
||||||
|
|
||||||
|
# Ensure we do not throw when the linked
|
||||||
|
# doorbell sensor is removed
|
||||||
|
hass.states.async_remove(doorbell_entity_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc.run()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert char.value is None
|
||||||
|
assert char2.value is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_lock_with_linked_doorbell_event(hass: HomeAssistant, hk_driver) -> None:
|
||||||
|
"""Test a lock with a linked doorbell event can update."""
|
||||||
|
await async_setup_component(hass, lock.DOMAIN, {lock.DOMAIN: {"platform": "demo"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
doorbell_entity_id = "event.doorbell"
|
||||||
|
code = "1234"
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
doorbell_entity_id,
|
||||||
|
dt_util.utcnow().isoformat(),
|
||||||
|
{ATTR_DEVICE_CLASS: EventDeviceClass.DOORBELL},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
entity_id = "lock.demo_lock"
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, None)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc = Lock(
|
||||||
|
hass,
|
||||||
|
hk_driver,
|
||||||
|
"Lock",
|
||||||
|
entity_id,
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
ATTR_CODE: code,
|
||||||
|
CONF_LINKED_DOORBELL_SENSOR: doorbell_entity_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
bridge = HomeBridge("hass", hk_driver, "Test Bridge")
|
||||||
|
bridge.add_accessory(acc)
|
||||||
|
|
||||||
|
acc.run()
|
||||||
|
|
||||||
|
assert acc.aid == 2
|
||||||
|
assert acc.category == 6 # DoorLock
|
||||||
|
|
||||||
|
service = acc.get_service(SERV_DOORBELL)
|
||||||
|
assert service
|
||||||
|
char = service.get_characteristic(CHAR_PROGRAMMABLE_SWITCH_EVENT)
|
||||||
|
assert char
|
||||||
|
|
||||||
|
assert char.value is None
|
||||||
|
|
||||||
|
service2 = acc.get_service(SERV_STATELESS_PROGRAMMABLE_SWITCH)
|
||||||
|
assert service2
|
||||||
|
char2 = service.get_characteristic(CHAR_PROGRAMMABLE_SWITCH_EVENT)
|
||||||
|
assert char2
|
||||||
|
broker = MagicMock()
|
||||||
|
char2.broker = broker
|
||||||
|
assert char2.value is None
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
doorbell_entity_id,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
{ATTR_DEVICE_CLASS: EventDeviceClass.DOORBELL},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert char.value is None
|
||||||
|
assert char2.value is None
|
||||||
|
assert len(broker.mock_calls) == 0
|
||||||
|
|
||||||
|
char.set_value(True)
|
||||||
|
char2.set_value(True)
|
||||||
|
broker.reset_mock()
|
||||||
|
|
||||||
|
original_time = dt_util.utcnow().isoformat()
|
||||||
|
hass.states.async_set(
|
||||||
|
doorbell_entity_id,
|
||||||
|
original_time,
|
||||||
|
{ATTR_DEVICE_CLASS: EventDeviceClass.DOORBELL},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert char.value is None
|
||||||
|
assert char2.value is None
|
||||||
|
assert len(broker.mock_calls) == 2
|
||||||
|
broker.reset_mock()
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
doorbell_entity_id,
|
||||||
|
original_time,
|
||||||
|
{ATTR_DEVICE_CLASS: EventDeviceClass.DOORBELL},
|
||||||
|
force_update=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert char.value is None
|
||||||
|
assert char2.value is None
|
||||||
|
assert len(broker.mock_calls) == 0
|
||||||
|
broker.reset_mock()
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
doorbell_entity_id,
|
||||||
|
original_time,
|
||||||
|
{ATTR_DEVICE_CLASS: EventDeviceClass.DOORBELL, "other": "attr"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert char.value is None
|
||||||
|
assert char2.value is None
|
||||||
|
assert len(broker.mock_calls) == 0
|
||||||
|
broker.reset_mock()
|
||||||
|
|
||||||
|
# Ensure we do not throw when the linked
|
||||||
|
# doorbell sensor is removed
|
||||||
|
hass.states.async_remove(doorbell_entity_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc.run()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert char.value is None
|
||||||
|
assert char2.value is None
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
hass.states.async_set(
|
||||||
|
doorbell_entity_id,
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
|
{ATTR_DEVICE_CLASS: EventDeviceClass.DOORBELL},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
# Ensure re-adding does not fire an event
|
||||||
|
assert not broker.mock_calls
|
||||||
|
broker.reset_mock()
|
||||||
|
|
||||||
|
# going from unavailable to a state should not fire an event
|
||||||
|
hass.states.async_set(
|
||||||
|
doorbell_entity_id,
|
||||||
|
dt_util.utcnow().isoformat(),
|
||||||
|
{ATTR_DEVICE_CLASS: EventDeviceClass.DOORBELL},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert not broker.mock_calls
|
||||||
|
|
||||||
|
# But a second update does
|
||||||
|
hass.states.async_set(
|
||||||
|
doorbell_entity_id,
|
||||||
|
dt_util.utcnow().isoformat(),
|
||||||
|
{ATTR_DEVICE_CLASS: EventDeviceClass.DOORBELL},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert broker.mock_calls
|
||||||
|
|
||||||
|
|
||||||
|
async def test_lock_with_a_missing_linked_doorbell_sensor(
|
||||||
|
hass: HomeAssistant, hk_driver
|
||||||
|
) -> None:
|
||||||
|
"""Test a lock with a configured linked doorbell sensor that is missing."""
|
||||||
|
await async_setup_component(hass, lock.DOMAIN, {lock.DOMAIN: {"platform": "demo"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
code = "1234"
|
||||||
|
doorbell_entity_id = "binary_sensor.doorbell"
|
||||||
|
entity_id = "lock.demo_lock"
|
||||||
|
hass.states.async_set(entity_id, None)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc = Lock(
|
||||||
|
hass,
|
||||||
|
hk_driver,
|
||||||
|
"Lock",
|
||||||
|
entity_id,
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
ATTR_CODE: code,
|
||||||
|
CONF_LINKED_DOORBELL_SENSOR: doorbell_entity_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
bridge = HomeBridge("hass", hk_driver, "Test Bridge")
|
||||||
|
bridge.add_accessory(acc)
|
||||||
|
|
||||||
|
acc.run()
|
||||||
|
|
||||||
|
assert acc.aid == 2
|
||||||
|
assert acc.category == 6 # DoorLock
|
||||||
|
|
||||||
|
assert not acc.get_service(SERV_DOORBELL)
|
||||||
|
assert not acc.get_service(SERV_STATELESS_PROGRAMMABLE_SWITCH)
|
||||||
|
|
|
@ -159,8 +159,20 @@ def test_validate_entity_config() -> None:
|
||||||
assert vec({"lock.demo": {}}) == {
|
assert vec({"lock.demo": {}}) == {
|
||||||
"lock.demo": {ATTR_CODE: None, CONF_LOW_BATTERY_THRESHOLD: 20}
|
"lock.demo": {ATTR_CODE: None, CONF_LOW_BATTERY_THRESHOLD: 20}
|
||||||
}
|
}
|
||||||
assert vec({"lock.demo": {ATTR_CODE: "1234"}}) == {
|
|
||||||
"lock.demo": {ATTR_CODE: "1234", CONF_LOW_BATTERY_THRESHOLD: 20}
|
assert vec(
|
||||||
|
{
|
||||||
|
"lock.demo": {
|
||||||
|
ATTR_CODE: "1234",
|
||||||
|
CONF_LINKED_DOORBELL_SENSOR: "event.doorbell",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) == {
|
||||||
|
"lock.demo": {
|
||||||
|
ATTR_CODE: "1234",
|
||||||
|
CONF_LOW_BATTERY_THRESHOLD: 20,
|
||||||
|
CONF_LINKED_DOORBELL_SENSOR: "event.doorbell",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert vec({"media_player.demo": {}}) == {
|
assert vec({"media_player.demo": {}}) == {
|
||||||
|
|
Loading…
Reference in New Issue