"""Tests for tedee lock.""" from datetime import timedelta from unittest.mock import MagicMock from urllib.parse import urlparse from freezegun.api import FrozenDateTimeFactory from pytedee_async import TedeeLock, TedeeLockState from pytedee_async.exception import ( TedeeClientException, TedeeDataUpdateException, TedeeLocalAuthException, ) import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, SERVICE_LOCK, SERVICE_OPEN, SERVICE_UNLOCK, STATE_LOCKED, STATE_LOCKING, STATE_UNLOCKED, STATE_UNLOCKING, ) from homeassistant.components.webhook import async_generate_url from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE 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 == STATE_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 == STATE_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 == STATE_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, match="Failed to lock the door. Lock 12345"): await hass.services.async_call( LOCK_DOMAIN, SERVICE_LOCK, { ATTR_ENTITY_ID: "lock.lock_1a2b", }, blocking=True, ) mock_tedee.unlock.side_effect = TedeeClientException("Boom") with pytest.raises( HomeAssistantError, match="Failed to unlock the door. Lock 12345" ): await hass.services.async_call( LOCK_DOMAIN, SERVICE_UNLOCK, { ATTR_ENTITY_ID: "lock.lock_1a2b", }, blocking=True, ) mock_tedee.open.side_effect = TedeeClientException("Boom") with pytest.raises( HomeAssistantError, match="Failed to unlatch the door. Lock 12345" ): await hass.services.async_call( LOCK_DOMAIN, SERVICE_OPEN, { ATTR_ENTITY_ID: "lock.lock_1a2b", }, blocking=True, ) @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 async def test_webhook_update( hass: HomeAssistant, mock_tedee: MagicMock, hass_client_no_auth: ClientSessionGenerator, ) -> None: """Test updated data set through webhook.""" state = hass.states.get("lock.lock_1a2b") assert state assert state.state == STATE_UNLOCKED webhook_data = {"dummystate": 6} mock_tedee.locks_dict[ 12345 ].state = TedeeLockState.LOCKED # is updated in the lib, so mock and assert in L296 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 == STATE_LOCKED