|
@@ -1,6 +1,9 @@
|
|
|
+from unittest.mock import AsyncMock, Mock, patch
|
|
|
+
|
|
|
import pytest
|
|
|
|
|
|
from switchbot import SwitchbotModel
|
|
|
+from switchbot.const.lock import LockStatus
|
|
|
from switchbot.devices import lock
|
|
|
|
|
|
from .test_adv_parser import generate_ble_device
|
|
@@ -40,3 +43,635 @@ def test_lock_init_with_invalid_model(model: str):
|
|
|
ValueError, match="initializing SwitchbotLock with a non-lock model"
|
|
|
):
|
|
|
create_device_for_command_testing(model)
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_verify_encryption_key(model: str):
|
|
|
+ """Test verify_encryption_key method."""
|
|
|
+ ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
|
+ with patch("switchbot.devices.lock.super") as mock_super:
|
|
|
+ mock_super().verify_encryption_key = AsyncMock(return_value=True)
|
|
|
+ result = await lock.SwitchbotLock.verify_encryption_key(
|
|
|
+ ble_device, "key_id", "encryption_key", model
|
|
|
+ )
|
|
|
+ assert result is True
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ ("model", "command"),
|
|
|
+ [
|
|
|
+ (SwitchbotModel.LOCK, b"W\x0fN\x01\x01\x10\x80"),
|
|
|
+ (SwitchbotModel.LOCK_LITE, b"W\x0fN\x01\x01\x10\x81"),
|
|
|
+ (SwitchbotModel.LOCK_PRO, b"W\x0fN\x01\x01\x10\x85"),
|
|
|
+ (SwitchbotModel.LOCK_ULTRA, b"W\x0fN\x01\x01\x10\x86"),
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_lock(model: str, command: bytes):
|
|
|
+ """Test lock method."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ device._get_adv_value = Mock(return_value=LockStatus.UNLOCKED)
|
|
|
+ with (
|
|
|
+ patch.object(device, "_send_command", return_value=b"\x01\x00"),
|
|
|
+ patch.object(device, "_enable_notifications", return_value=True),
|
|
|
+ patch.object(device, "_get_basic_info", return_value=b"\x00\x64\x01"),
|
|
|
+ ):
|
|
|
+ result = await device.lock()
|
|
|
+ assert result is True
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ ("model", "command"),
|
|
|
+ [
|
|
|
+ (SwitchbotModel.LOCK, b"W\x0fN\x01\x01\x10\x80"),
|
|
|
+ (SwitchbotModel.LOCK_LITE, b"W\x0fN\x01\x01\x10\x81"),
|
|
|
+ (SwitchbotModel.LOCK_PRO, b"W\x0fN\x01\x01\x10\x84"),
|
|
|
+ (SwitchbotModel.LOCK_ULTRA, b"W\x0fN\x01\x01\x10\x83"),
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_unlock(model: str, command: bytes):
|
|
|
+ """Test unlock method."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ device._get_adv_value = Mock(return_value=LockStatus.LOCKED)
|
|
|
+ with (
|
|
|
+ patch.object(device, "_send_command", return_value=b"\x01\x00"),
|
|
|
+ patch.object(device, "_enable_notifications", return_value=True),
|
|
|
+ patch.object(device, "_get_basic_info", return_value=b"\x00\x64\x01"),
|
|
|
+ ):
|
|
|
+ result = await device.unlock()
|
|
|
+ assert result is True
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_unlock_without_unlatch(model: str):
|
|
|
+ """Test unlock_without_unlatch method."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ device._get_adv_value = Mock(return_value=LockStatus.LOCKED)
|
|
|
+ with (
|
|
|
+ patch.object(device, "_send_command", return_value=b"\x01\x00"),
|
|
|
+ patch.object(device, "_enable_notifications", return_value=True),
|
|
|
+ patch.object(device, "_get_basic_info", return_value=b"\x00\x64\x01"),
|
|
|
+ ):
|
|
|
+ result = await device.unlock_without_unlatch()
|
|
|
+ assert result is True
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_get_basic_info(model: str):
|
|
|
+ """Test get_basic_info method."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ lock_data = b"\x00\x80\x00\x00\x00\x00\x00\x00"
|
|
|
+ basic_data = b"\x00\x64\x01"
|
|
|
+ with (
|
|
|
+ patch.object(device, "_get_lock_info", return_value=lock_data),
|
|
|
+ patch.object(device, "_get_basic_info", return_value=basic_data),
|
|
|
+ ):
|
|
|
+ result = await device.get_basic_info()
|
|
|
+ assert result is not None
|
|
|
+ assert "battery" in result
|
|
|
+ assert "firmware" in result
|
|
|
+ assert "calibration" in result
|
|
|
+ assert "status" in result
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_get_basic_info_no_lock_data(model: str):
|
|
|
+ """Test get_basic_info when no lock data is returned."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ with patch.object(device, "_get_lock_info", return_value=None):
|
|
|
+ result = await device.get_basic_info()
|
|
|
+ assert result is None
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_get_basic_info_no_basic_data(model: str):
|
|
|
+ """Test get_basic_info when no basic data is returned."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ lock_data = b"\x00\x80\x00\x00\x00\x00\x00\x00"
|
|
|
+ with (
|
|
|
+ patch.object(device, "_get_lock_info", return_value=lock_data),
|
|
|
+ patch.object(device, "_get_basic_info", return_value=None),
|
|
|
+ ):
|
|
|
+ result = await device.get_basic_info()
|
|
|
+ assert result is None
|
|
|
+
|
|
|
+
|
|
|
+def test_parse_basic_data():
|
|
|
+ """Test _parse_basic_data method."""
|
|
|
+ device = create_device_for_command_testing(SwitchbotModel.LOCK)
|
|
|
+ basic_data = b"\x00\x64\x01"
|
|
|
+ result = device._parse_basic_data(basic_data)
|
|
|
+ assert result["battery"] == 100
|
|
|
+ assert result["firmware"] == 0.1
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+def test_is_calibrated(model: str):
|
|
|
+ """Test is_calibrated method."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ device._get_adv_value = Mock(return_value=True)
|
|
|
+ assert device.is_calibrated() is True
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+def test_get_lock_status(model: str):
|
|
|
+ """Test get_lock_status method."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ device._get_adv_value = Mock(return_value=LockStatus.LOCKED)
|
|
|
+ assert device.get_lock_status() == LockStatus.LOCKED
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+def test_is_door_open(model: str):
|
|
|
+ """Test is_door_open method."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ device._get_adv_value = Mock(return_value=True)
|
|
|
+ assert device.is_door_open() is True
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+def test_is_unclosed_alarm_on(model: str):
|
|
|
+ """Test is_unclosed_alarm_on method."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ device._get_adv_value = Mock(return_value=True)
|
|
|
+ assert device.is_unclosed_alarm_on() is True
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+def test_is_unlocked_alarm_on(model: str):
|
|
|
+ """Test is_unlocked_alarm_on method."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ device._get_adv_value = Mock(return_value=True)
|
|
|
+ assert device.is_unlocked_alarm_on() is True
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ ],
|
|
|
+)
|
|
|
+def test_is_auto_lock_paused(model: str):
|
|
|
+ """Test is_auto_lock_paused method."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ device._get_adv_value = Mock(return_value=True)
|
|
|
+ assert device.is_auto_lock_paused() is True
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+def test_is_night_latch_enabled(model: str):
|
|
|
+ """Test is_night_latch_enabled method."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ device._get_adv_value = Mock(return_value=True)
|
|
|
+ assert device.is_night_latch_enabled() is True
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_get_lock_info(model: str):
|
|
|
+ """Test _get_lock_info method."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ expected_data = b"\x01\x00\x80\x00\x00\x00\x00\x00"
|
|
|
+ with patch.object(device, "_send_command", return_value=expected_data):
|
|
|
+ result = await device._get_lock_info()
|
|
|
+ assert result == expected_data
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_get_lock_info_failure(model: str):
|
|
|
+ """Test _get_lock_info method when command fails."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ with patch.object(device, "_send_command", return_value=b"\x00\x00"):
|
|
|
+ result = await device._get_lock_info()
|
|
|
+ assert result is None
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_enable_notifications(model: str):
|
|
|
+ """Test _enable_notifications method."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ with patch.object(device, "_send_command", return_value=b"\x01\x00"):
|
|
|
+ result = await device._enable_notifications()
|
|
|
+ assert result is True
|
|
|
+ assert device._notifications_enabled is True
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_enable_notifications_already_enabled(model: str):
|
|
|
+ """Test _enable_notifications when already enabled."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ device._notifications_enabled = True
|
|
|
+ with patch.object(device, "_send_command") as mock_send:
|
|
|
+ result = await device._enable_notifications()
|
|
|
+ assert result is True
|
|
|
+ mock_send.assert_not_called()
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_disable_notifications(model: str):
|
|
|
+ """Test _disable_notifications method."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ device._notifications_enabled = True
|
|
|
+ with patch.object(device, "_send_command", return_value=b"\x01\x00"):
|
|
|
+ result = await device._disable_notifications()
|
|
|
+ assert result is True
|
|
|
+ assert device._notifications_enabled is False
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_disable_notifications_already_disabled(model: str):
|
|
|
+ """Test _disable_notifications when already disabled."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ device._notifications_enabled = False
|
|
|
+ with patch.object(device, "_send_command") as mock_send:
|
|
|
+ result = await device._disable_notifications()
|
|
|
+ assert result is True
|
|
|
+ mock_send.assert_not_called()
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+def test_notification_handler(model: str):
|
|
|
+ """Test _notification_handler method."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ device._notifications_enabled = True
|
|
|
+ data = bytearray(b"\x0f\x00\x00\x00\x80\x00\x00\x00\x00\x00")
|
|
|
+ with patch.object(device, "_update_lock_status") as mock_update:
|
|
|
+ device._notification_handler(0, data)
|
|
|
+ mock_update.assert_called_once_with(data)
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+def test_notification_handler_not_enabled(model: str):
|
|
|
+ """Test _notification_handler when notifications not enabled."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ device._notifications_enabled = False
|
|
|
+ data = bytearray(b"\x0f\x00\x00\x00\x80\x00\x00\x00\x00\x00")
|
|
|
+ with (
|
|
|
+ patch.object(device, "_update_lock_status") as mock_update,
|
|
|
+ patch.object(
|
|
|
+ device.__class__.__bases__[0], "_notification_handler"
|
|
|
+ ) as mock_super,
|
|
|
+ ):
|
|
|
+ device._notification_handler(0, data)
|
|
|
+ mock_update.assert_not_called()
|
|
|
+ mock_super.assert_called_once()
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+def test_update_lock_status(model: str):
|
|
|
+ """Test _update_lock_status method."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ data = bytearray(b"\x0f\x00\x00\x00\x80\x00\x00\x00\x00\x00")
|
|
|
+ with (
|
|
|
+ patch.object(device, "_decrypt", return_value=b"\x80\x00\x00\x00\x00\x00"),
|
|
|
+ patch.object(device, "_update_parsed_data", return_value=True),
|
|
|
+ patch.object(device, "_reset_disconnect_timer"),
|
|
|
+ patch.object(device, "_fire_callbacks"),
|
|
|
+ ):
|
|
|
+ device._update_lock_status(data)
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ ("model", "data", "expected"),
|
|
|
+ [
|
|
|
+ (
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ b"\x80\x00\x00\x00\x00\x00",
|
|
|
+ {
|
|
|
+ "calibration": True,
|
|
|
+ "status": LockStatus.LOCKED, # (0x80 & 0b01110000) >> 4 = 0 = LOCKED
|
|
|
+ "door_open": False,
|
|
|
+ "unclosed_alarm": False,
|
|
|
+ "unlocked_alarm": False,
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ (
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ b"\x80\x00\x00\x00\x00\x00",
|
|
|
+ {
|
|
|
+ "calibration": True,
|
|
|
+ "status": LockStatus.LOCKED, # (0x80 & 0b01110000) >> 4 = 0 = LOCKED
|
|
|
+ "unlocked_alarm": False,
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ (
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ b"\x80\x00\x00\x00\x00\x00",
|
|
|
+ {
|
|
|
+ "calibration": True,
|
|
|
+ "status": LockStatus.LOCKED, # (0x80 & 0b01111000) >> 3 = 0 = LOCKED
|
|
|
+ "door_open": False,
|
|
|
+ "unclosed_alarm": False,
|
|
|
+ "unlocked_alarm": False,
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ (
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ b"\x88\x10\x00\x00\x00\xc0",
|
|
|
+ {
|
|
|
+ "calibration": True,
|
|
|
+ "status": LockStatus.UNLOCKED, # (0x88 & 0b01111000) >> 3 = 0x08 >> 3 = 1 = UNLOCKED
|
|
|
+ "door_open": True,
|
|
|
+ "unclosed_alarm": True,
|
|
|
+ "unlocked_alarm": True,
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+)
|
|
|
+def test_parse_lock_data(model: str, data: bytes, expected: dict):
|
|
|
+ """Test _parse_lock_data static method."""
|
|
|
+ result = lock.SwitchbotLock._parse_lock_data(data, model)
|
|
|
+ assert result == expected
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ ("model", "data", "expected"),
|
|
|
+ [
|
|
|
+ # Test LOCK with different status bits and flags
|
|
|
+ (
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ b"\x94\x00\x00\x00\x00\x00", # Unlocked status (0x10 >> 4 = 1) with door open
|
|
|
+ {
|
|
|
+ "calibration": True,
|
|
|
+ "status": LockStatus.UNLOCKED,
|
|
|
+ "door_open": True,
|
|
|
+ "unclosed_alarm": False,
|
|
|
+ "unlocked_alarm": False,
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ # Test LOCK_LITE without door_open field
|
|
|
+ (
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ b"\x90\x10\x00\x00\x00\x00", # Unlocked with unlocked alarm
|
|
|
+ {
|
|
|
+ "calibration": True,
|
|
|
+ "status": LockStatus.UNLOCKED,
|
|
|
+ "unlocked_alarm": True,
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ # Test LOCK_PRO with new bit positions
|
|
|
+ (
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ b"\x90\x10\x00\x00\x00\xc0", # New format: status bits 3-6, door open bit 4 of byte 1
|
|
|
+ {
|
|
|
+ "calibration": True,
|
|
|
+ "status": LockStatus.LOCKING, # (0x90 & 0b01111000) >> 3 = 0x10 >> 3 = 2 (LOCKING)
|
|
|
+ "door_open": True, # bit 4 of byte 1 (0x10)
|
|
|
+ "unclosed_alarm": True, # bit 7 of byte 5 (0xc0)
|
|
|
+ "unlocked_alarm": True, # bit 6 of byte 5 (0xc0)
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ # Test LOCK_ULTRA with same format as PRO
|
|
|
+ (
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ b"\x88\x00\x00\x00\x00\x40", # Unlocked with unlocked alarm only
|
|
|
+ {
|
|
|
+ "calibration": True,
|
|
|
+ "status": LockStatus.UNLOCKED, # (0x88 & 0b01111000) >> 3 = 0x08 >> 3 = 1
|
|
|
+ "door_open": False,
|
|
|
+ "unclosed_alarm": False,
|
|
|
+ "unlocked_alarm": True, # bit 6 of byte 5
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+)
|
|
|
+def test_parse_lock_data_new_formats(model: str, data: bytes, expected: dict):
|
|
|
+ """Test _parse_lock_data with new format changes."""
|
|
|
+ result = lock.SwitchbotLock._parse_lock_data(data, model)
|
|
|
+ assert result == expected
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_lock_with_update(model: str):
|
|
|
+ """Test lock method with status update."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ device._get_adv_value = Mock(side_effect=[None, LockStatus.UNLOCKED])
|
|
|
+ with (
|
|
|
+ patch.object(device, "update", new_callable=AsyncMock),
|
|
|
+ patch.object(device, "_send_command", return_value=b"\x01\x00"),
|
|
|
+ patch.object(device, "_enable_notifications", return_value=True),
|
|
|
+ patch.object(device, "_get_basic_info", return_value=b"\x00\x64\x01"),
|
|
|
+ ):
|
|
|
+ result = await device.lock()
|
|
|
+ assert result is True
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ ("model", "status"),
|
|
|
+ [
|
|
|
+ (SwitchbotModel.LOCK, LockStatus.LOCKED),
|
|
|
+ (SwitchbotModel.LOCK_LITE, LockStatus.LOCKING),
|
|
|
+ (SwitchbotModel.LOCK_PRO, LockStatus.LOCKED),
|
|
|
+ (SwitchbotModel.LOCK_ULTRA, LockStatus.LOCKING),
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_lock_already_locked(model: str, status: LockStatus):
|
|
|
+ """Test lock method when already locked."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ device._get_adv_value = Mock(return_value=status)
|
|
|
+ with patch.object(device, "_send_command") as mock_send:
|
|
|
+ result = await device.lock()
|
|
|
+ assert result is True
|
|
|
+ mock_send.assert_not_called()
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "model",
|
|
|
+ [
|
|
|
+ SwitchbotModel.LOCK,
|
|
|
+ SwitchbotModel.LOCK_LITE,
|
|
|
+ SwitchbotModel.LOCK_PRO,
|
|
|
+ SwitchbotModel.LOCK_ULTRA,
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_lock_with_invalid_basic_data(model: str):
|
|
|
+ """Test lock method with invalid basic data."""
|
|
|
+ device = create_device_for_command_testing(model)
|
|
|
+ device._get_adv_value = Mock(return_value=LockStatus.UNLOCKED)
|
|
|
+ with (
|
|
|
+ patch.object(device, "_send_command", return_value=b"\x01\x00"),
|
|
|
+ patch.object(device, "_enable_notifications", return_value=True),
|
|
|
+ patch.object(device, "_get_basic_info", return_value=b"\x00"),
|
|
|
+ ):
|
|
|
+ result = await device.lock()
|
|
|
+ assert result is True
|