core/tests/components/tedee/test_lock.py

312 lines
8.5 KiB
Python

"""Tests for tedee lock."""
from datetime import timedelta
from unittest.mock import MagicMock
from urllib.parse import urlparse
from aiotedee import TedeeLock, TedeeLockState
from aiotedee.exception import (
TedeeClientException,
TedeeDataUpdateException,
TedeeLocalAuthException,
)
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.lock import (
DOMAIN as LOCK_DOMAIN,
SERVICE_LOCK,
SERVICE_OPEN,
SERVICE_UNLOCK,
LockState,
)
from homeassistant.components.webhook import async_generate_url
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .conftest import WEBHOOK_ID
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.typing import ClientSessionGenerator
pytestmark = pytest.mark.usefixtures("init_integration")
async def test_lock(
hass: HomeAssistant,
mock_tedee: MagicMock,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the tedee lock."""
mock_tedee.lock.return_value = None
mock_tedee.unlock.return_value = None
mock_tedee.open.return_value = None
state = hass.states.get("lock.lock_1a2b")
assert state
assert state == snapshot
entry = entity_registry.async_get(state.entity_id)
assert entry
assert entry == snapshot
assert entry.device_id
device = device_registry.async_get(entry.device_id)
assert device == snapshot
await hass.services.async_call(
LOCK_DOMAIN,
SERVICE_LOCK,
{
ATTR_ENTITY_ID: "lock.lock_1a2b",
},
blocking=True,
)
assert len(mock_tedee.lock.mock_calls) == 1
mock_tedee.lock.assert_called_once_with(12345)
state = hass.states.get("lock.lock_1a2b")
assert state
assert state.state == LockState.LOCKING
await hass.services.async_call(
LOCK_DOMAIN,
SERVICE_UNLOCK,
{
ATTR_ENTITY_ID: "lock.lock_1a2b",
},
blocking=True,
)
assert len(mock_tedee.unlock.mock_calls) == 1
mock_tedee.unlock.assert_called_once_with(12345)
state = hass.states.get("lock.lock_1a2b")
assert state
assert state.state == LockState.UNLOCKING
await hass.services.async_call(
LOCK_DOMAIN,
SERVICE_OPEN,
{
ATTR_ENTITY_ID: "lock.lock_1a2b",
},
blocking=True,
)
assert len(mock_tedee.open.mock_calls) == 1
mock_tedee.open.assert_called_once_with(12345)
state = hass.states.get("lock.lock_1a2b")
assert state
assert state.state == LockState.UNLOCKING
async def test_lock_without_pullspring(
hass: HomeAssistant,
mock_tedee: MagicMock,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the tedee lock without pullspring."""
mock_tedee.lock.return_value = None
mock_tedee.unlock.return_value = None
mock_tedee.open.return_value = None
state = hass.states.get("lock.lock_2c3d")
assert state
assert state == snapshot
entry = entity_registry.async_get(state.entity_id)
assert entry
assert entry == snapshot
assert entry.device_id
device = device_registry.async_get(entry.device_id)
assert device
assert device == snapshot
with pytest.raises(
HomeAssistantError,
match="Entity lock.lock_2c3d does not support this service.",
):
await hass.services.async_call(
LOCK_DOMAIN,
SERVICE_OPEN,
{
ATTR_ENTITY_ID: "lock.lock_2c3d",
},
blocking=True,
)
assert len(mock_tedee.open.mock_calls) == 0
async def test_lock_errors(
hass: HomeAssistant,
mock_tedee: MagicMock,
) -> None:
"""Test event errors."""
mock_tedee.lock.side_effect = TedeeClientException("Boom")
with pytest.raises(HomeAssistantError) as exc_info:
await hass.services.async_call(
LOCK_DOMAIN,
SERVICE_LOCK,
{
ATTR_ENTITY_ID: "lock.lock_1a2b",
},
blocking=True,
)
assert exc_info.value.translation_key == "lock_failed"
mock_tedee.unlock.side_effect = TedeeClientException("Boom")
with pytest.raises(HomeAssistantError) as exc_info:
await hass.services.async_call(
LOCK_DOMAIN,
SERVICE_UNLOCK,
{
ATTR_ENTITY_ID: "lock.lock_1a2b",
},
blocking=True,
)
assert exc_info.value.translation_key == "unlock_failed"
mock_tedee.open.side_effect = TedeeClientException("Boom")
with pytest.raises(HomeAssistantError) as exc_info:
await hass.services.async_call(
LOCK_DOMAIN,
SERVICE_OPEN,
{
ATTR_ENTITY_ID: "lock.lock_1a2b",
},
blocking=True,
)
assert exc_info.value.translation_key == "open_failed"
@pytest.mark.parametrize(
"side_effect",
[
TedeeClientException("Boom"),
TedeeLocalAuthException("Boom"),
TimeoutError,
TedeeDataUpdateException("Boom"),
],
)
async def test_update_failed(
hass: HomeAssistant,
mock_tedee: MagicMock,
freezer: FrozenDateTimeFactory,
side_effect: Exception,
) -> None:
"""Test update failed."""
mock_tedee.sync.side_effect = side_effect
freezer.tick(timedelta(minutes=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("lock.lock_1a2b")
assert state is not None
assert state.state == STATE_UNAVAILABLE
async def test_cleanup_removed_locks(
hass: HomeAssistant,
mock_tedee: MagicMock,
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Ensure removed locks are cleaned up."""
devices = dr.async_entries_for_config_entry(
device_registry, mock_config_entry.entry_id
)
locks = [device.name for device in devices]
assert "Lock-1A2B" in locks
# remove a lock and wait for coordinator
mock_tedee.locks_dict.pop(12345)
freezer.tick(timedelta(minutes=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
devices = dr.async_entries_for_config_entry(
device_registry, mock_config_entry.entry_id
)
locks = [device.name for device in devices]
assert "Lock-1A2B" not in locks
async def test_new_lock(
hass: HomeAssistant,
mock_tedee: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Ensure new lock is added automatically."""
state = hass.states.get("lock.lock_4e5f")
assert state is None
mock_tedee.locks_dict[666666] = TedeeLock("Lock-4E5F", 666666, 2)
mock_tedee.locks_dict[777777] = TedeeLock(
"Lock-6G7H",
777777,
4,
is_enabled_pullspring=True,
)
freezer.tick(timedelta(minutes=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("lock.lock_4e5f")
assert state
state = hass.states.get("lock.lock_6g7h")
assert state
@pytest.mark.parametrize(
("lib_state", "expected_state"),
[
(TedeeLockState.LOCKED, LockState.LOCKED),
(TedeeLockState.HALF_OPEN, STATE_UNKNOWN),
(TedeeLockState.UNKNOWN, STATE_UNKNOWN),
(TedeeLockState.UNCALIBRATED, STATE_UNAVAILABLE),
],
)
async def test_webhook_update(
hass: HomeAssistant,
mock_tedee: MagicMock,
hass_client_no_auth: ClientSessionGenerator,
lib_state: TedeeLockState,
expected_state: str,
) -> None:
"""Test updated data set through webhook."""
state = hass.states.get("lock.lock_1a2b")
assert state
assert state.state == LockState.UNLOCKED
webhook_data = {"dummystate": lib_state.value}
# is updated in the lib, so mock and assert below
mock_tedee.locks_dict[12345].state = lib_state
client = await hass_client_no_auth()
webhook_url = async_generate_url(hass, WEBHOOK_ID)
await client.post(
urlparse(webhook_url).path,
json=webhook_data,
)
mock_tedee.parse_webhook_message.assert_called_once_with(webhook_data)
state = hass.states.get("lock.lock_1a2b")
assert state
assert state.state == expected_state