"""Test the switchbot locks.""" from collections.abc import Callable from unittest.mock import AsyncMock, MagicMock, patch import pytest from switchbot import LockStatus, SwitchbotOperationError from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockState from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_LOCK, SERVICE_OPEN, SERVICE_UNLOCK, STATE_OFF, STATE_ON, STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from . import ( LOCK_LITE_SERVICE_INFO, LOCK_PRO_WIFI_SERVICE_INFO, LOCK_SERVICE_INFO, LOCK_ULTRA_SERVICE_INFO, LOCK_VISION_PRO_SERVICE_INFO, LOCK_VISION_SERVICE_INFO, WOLOCKPRO_SERVICE_INFO, ) from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info @pytest.mark.parametrize( ("sensor_type", "service_info"), [ ("lock_pro", WOLOCKPRO_SERVICE_INFO), ("lock", LOCK_SERVICE_INFO), ("lock_lite", LOCK_LITE_SERVICE_INFO), ("lock_ultra", LOCK_ULTRA_SERVICE_INFO), ("lock_vision", LOCK_VISION_SERVICE_INFO), ("lock_vision_pro", LOCK_VISION_PRO_SERVICE_INFO), ("lock_pro_wifi", LOCK_PRO_WIFI_SERVICE_INFO), ], ) @pytest.mark.parametrize( ("service", "mock_method"), [(SERVICE_UNLOCK, "unlock"), (SERVICE_LOCK, "lock")], ) async def test_lock_services( hass: HomeAssistant, mock_entry_encrypted_factory: Callable[[str], MockConfigEntry], sensor_type: str, service: str, mock_method: str, service_info: BluetoothServiceInfoBleak, ) -> None: """Test lock and unlock services on lock and lockpro devices.""" inject_bluetooth_service_info(hass, service_info) entry = mock_entry_encrypted_factory(sensor_type=sensor_type) entry.add_to_hass(hass) mocked_instance = AsyncMock(return_value=True) with patch.multiple( "homeassistant.components.switchbot.lock.switchbot.SwitchbotLock", update=AsyncMock(return_value=None), **{mock_method: mocked_instance}, ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() entity_id = "lock.test_name" await hass.services.async_call( LOCK_DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True, ) mocked_instance.assert_awaited_once() @pytest.mark.parametrize( ("sensor_type", "service_info"), [ ("lock_pro", WOLOCKPRO_SERVICE_INFO), ("lock", LOCK_SERVICE_INFO), ("lock_lite", LOCK_LITE_SERVICE_INFO), ("lock_ultra", LOCK_ULTRA_SERVICE_INFO), ("lock_vision", LOCK_VISION_SERVICE_INFO), ("lock_vision_pro", LOCK_VISION_PRO_SERVICE_INFO), ("lock_pro_wifi", LOCK_PRO_WIFI_SERVICE_INFO), ], ) @pytest.mark.parametrize( ("service", "mock_method"), [(SERVICE_UNLOCK, "unlock_without_unlatch"), (SERVICE_OPEN, "unlock")], ) async def test_lock_services_with_night_latch_enabled( hass: HomeAssistant, mock_entry_encrypted_factory: Callable[[str], MockConfigEntry], sensor_type: str, service: str, mock_method: str, service_info: BluetoothServiceInfoBleak, ) -> None: """Test lock service when night latch enabled.""" inject_bluetooth_service_info(hass, service_info) entry = mock_entry_encrypted_factory(sensor_type=sensor_type) entry.add_to_hass(hass) mocked_instance = AsyncMock(return_value=True) with patch.multiple( "homeassistant.components.switchbot.lock.switchbot.SwitchbotLock", is_night_latch_enabled=MagicMock(return_value=True), update=AsyncMock(return_value=None), **{mock_method: mocked_instance}, ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() entity_id = "lock.test_name" await hass.services.async_call( LOCK_DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True, ) mocked_instance.assert_awaited_once() @pytest.mark.parametrize( ("exception", "error_message"), [ ( SwitchbotOperationError("Operation failed"), "An error occurred while performing the action: Operation failed", ), ], ) @pytest.mark.parametrize( ("service", "mock_method"), [ (SERVICE_LOCK, "lock"), (SERVICE_OPEN, "unlock"), (SERVICE_UNLOCK, "unlock_without_unlatch"), ], ) async def test_exception_handling_lock_service( hass: HomeAssistant, mock_entry_encrypted_factory: Callable[[str], MockConfigEntry], service: str, mock_method: str, exception: Exception, error_message: str, ) -> None: """Test exception handling for lock service with exception.""" inject_bluetooth_service_info(hass, LOCK_SERVICE_INFO) entry = mock_entry_encrypted_factory(sensor_type="lock") entry.add_to_hass(hass) entity_id = "lock.test_name" with patch.multiple( "homeassistant.components.switchbot.lock.switchbot.SwitchbotLock", is_night_latch_enabled=MagicMock(return_value=True), update=AsyncMock(return_value=None), **{mock_method: AsyncMock(side_effect=exception)}, ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() with pytest.raises(HomeAssistantError, match=error_message): await hass.services.async_call( LOCK_DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True, ) async def test_lock_ultra_half_lock_state_shows_locked( hass: HomeAssistant, mock_entry_encrypted_factory: Callable[[str], MockConfigEntry], ) -> None: """Test that Lock Ultra in HALF_LOCKED state shows as locked in HA UI.""" inject_bluetooth_service_info(hass, LOCK_ULTRA_SERVICE_INFO) entry = mock_entry_encrypted_factory(sensor_type="lock_ultra") entry.add_to_hass(hass) with patch.multiple( "homeassistant.components.switchbot.lock.switchbot.SwitchbotLock", update=AsyncMock(return_value=None), get_lock_status=MagicMock(return_value=LockStatus.HALF_LOCKED), ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() lock_state = hass.states.get("lock.test_name") assert lock_state is not None assert lock_state.state == LockState.LOCKED @pytest.mark.parametrize( ("lock_status", "expected_state"), [ (LockStatus.HALF_LOCKED, STATE_ON), (LockStatus.LOCKED, STATE_OFF), ], ) async def test_lock_ultra_half_locked_binary_sensor( hass: HomeAssistant, mock_entry_encrypted_factory: Callable[[str], MockConfigEntry], lock_status: LockStatus, expected_state: str, ) -> None: """Test half_locked binary sensor state reflects Lock Ultra lock status.""" inject_bluetooth_service_info(hass, LOCK_ULTRA_SERVICE_INFO) entry = mock_entry_encrypted_factory(sensor_type="lock_ultra") entry.add_to_hass(hass) with patch.multiple( "homeassistant.components.switchbot.lock.switchbot.SwitchbotLock", update=AsyncMock(return_value=None), get_lock_status=MagicMock(return_value=lock_status), ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() sensor_state = hass.states.get("binary_sensor.test_name_half_locked") assert sensor_state is not None assert sensor_state.state == expected_state async def test_non_lock_ultra_no_half_locked_binary_sensor( hass: HomeAssistant, mock_entry_encrypted_factory: Callable[[str], MockConfigEntry], ) -> None: """Test half_locked binary sensor is absent for non-Lock-Ultra devices.""" inject_bluetooth_service_info(hass, LOCK_SERVICE_INFO) entry = mock_entry_encrypted_factory(sensor_type="lock") entry.add_to_hass(hass) with patch.multiple( "homeassistant.components.switchbot.lock.switchbot.SwitchbotLock", update=AsyncMock(return_value=None), ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert hass.states.get("binary_sensor.test_name_half_locked") is None async def test_lock_ultra_half_locked_binary_sensor_unknown( hass: HomeAssistant, mock_entry_encrypted_factory: Callable[[str], MockConfigEntry], ) -> None: """Test half_locked binary sensor is unknown when lock status is unavailable.""" inject_bluetooth_service_info(hass, LOCK_ULTRA_SERVICE_INFO) entry = mock_entry_encrypted_factory(sensor_type="lock_ultra") entry.add_to_hass(hass) with patch.multiple( "homeassistant.components.switchbot.lock.switchbot.SwitchbotLock", update=AsyncMock(return_value=None), get_lock_status=MagicMock(return_value=None), ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() sensor_state = hass.states.get("binary_sensor.test_name_half_locked") assert sensor_state is not None assert sensor_state.state == STATE_UNKNOWN