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.http import KEY_HASS, HomeAssistantView
|
||||
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.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
|
@ -1133,6 +1134,8 @@ class HomeKit:
|
|||
config[entity_id].setdefault(
|
||||
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):
|
||||
config[entity_id].setdefault(
|
||||
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 .accessories import TYPES, HomeAccessory, HomeDriver
|
||||
from .accessories import TYPES, HomeDriver
|
||||
from .const import (
|
||||
CHAR_MOTION_DETECTED,
|
||||
CHAR_MUTE,
|
||||
CHAR_PROGRAMMABLE_SWITCH_EVENT,
|
||||
CONF_AUDIO_CODEC,
|
||||
CONF_AUDIO_MAP,
|
||||
CONF_AUDIO_PACKET_SIZE,
|
||||
CONF_LINKED_DOORBELL_SENSOR,
|
||||
CONF_LINKED_MOTION_SENSOR,
|
||||
CONF_MAX_FPS,
|
||||
CONF_MAX_HEIGHT,
|
||||
|
@ -64,18 +61,13 @@ from .const import (
|
|||
DEFAULT_VIDEO_MAP,
|
||||
DEFAULT_VIDEO_PACKET_SIZE,
|
||||
DEFAULT_VIDEO_PROFILE_NAMES,
|
||||
SERV_DOORBELL,
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOORBELL_SINGLE_PRESS = 0
|
||||
DOORBELL_DOUBLE_PRESS = 1
|
||||
DOORBELL_LONG_PRESS = 2
|
||||
|
||||
VIDEO_OUTPUT = (
|
||||
"-map {v_map} -an "
|
||||
|
@ -149,7 +141,7 @@ CONFIG_DEFAULTS = {
|
|||
@TYPES.register("Camera")
|
||||
# False-positive on pylint, not a CameraEntity
|
||||
# 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."""
|
||||
|
||||
def __init__(
|
||||
|
@ -237,36 +229,6 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc]
|
|||
)
|
||||
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]
|
||||
@callback
|
||||
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()
|
||||
|
||||
@callback
|
||||
|
@ -344,39 +295,6 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc]
|
|||
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
|
||||
def async_update_state(self, new_state: State | None) -> None:
|
||||
"""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.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 .doorbell import HomeDoorbellAccessory
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -53,7 +54,7 @@ STATE_TO_SERVICE = {
|
|||
|
||||
|
||||
@TYPES.register("Lock")
|
||||
class Lock(HomeAccessory):
|
||||
class Lock(HomeDoorbellAccessory):
|
||||
"""Generate a Lock accessory for a lock entity.
|
||||
|
||||
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)}
|
||||
)
|
||||
|
||||
|
||||
COVER_SCHEMA = BASIC_INFO_SCHEMA.extend(
|
||||
{
|
||||
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)}
|
||||
)
|
||||
|
||||
LOCK_SCHEMA = CODE_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_LINKED_DOORBELL_SENSOR): cv.entity_domain(
|
||||
[binary_sensor.DOMAIN, EVENT_DOMAIN]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
MEDIA_PLAYER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_FEATURE): vol.All(
|
||||
|
@ -284,7 +291,7 @@ def validate_entity_config(values: dict) -> dict[str, dict]:
|
|||
if not isinstance(config, dict):
|
||||
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)
|
||||
|
||||
elif domain == media_player.const.DOMAIN:
|
||||
|
@ -301,6 +308,9 @@ def validate_entity_config(values: dict) -> dict[str, dict]:
|
|||
elif domain == "camera":
|
||||
config = CAMERA_SCHEMA(config)
|
||||
|
||||
elif domain == "lock":
|
||||
config = LOCK_SCHEMA(config)
|
||||
|
||||
elif domain == "switch":
|
||||
config = SWITCH_TYPE_SCHEMA(config)
|
||||
|
||||
|
|
|
@ -1,17 +1,34 @@
|
|||
"""Test different accessory types: Locks."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
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.lock import DOMAIN as LOCK_DOMAIN, LockState
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE,
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ENTITY_ID,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
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
|
||||
|
||||
|
@ -135,3 +152,285 @@ async def test_no_code(
|
|||
assert acc.char_target_state.value == 1
|
||||
assert len(events) == 1
|
||||
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": {}}) == {
|
||||
"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": {}}) == {
|
||||
|
|
Loading…
Reference in New Issue