test_bot.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. from unittest.mock import AsyncMock, Mock
  2. import pytest
  3. from bleak.backends.device import BLEDevice
  4. from switchbot import (
  5. SwitchBotAdvertisement,
  6. SwitchbotModel,
  7. SwitchbotOperationError,
  8. )
  9. from switchbot.devices import bot
  10. from .test_adv_parser import generate_ble_device
  11. def make_advertisement_data(
  12. ble_device: BLEDevice,
  13. ) -> SwitchBotAdvertisement:
  14. """Create Bot advertisement data for tests."""
  15. data: dict[str, bool | int | None] = {
  16. "battery": 89,
  17. "isOn": True,
  18. "switchMode": True,
  19. }
  20. return SwitchBotAdvertisement(
  21. address=ble_device.address,
  22. data={
  23. "rawAdvData": b"H\x90\xd9",
  24. "data": data,
  25. "isEncrypted": False,
  26. "model": "H",
  27. "modelFriendlyName": "Bot",
  28. "modelName": SwitchbotModel.BOT,
  29. },
  30. device=ble_device,
  31. rssi=-80,
  32. active=True,
  33. )
  34. def create_device_for_command_testing(
  35. *,
  36. inverse_direction: bool | None = None,
  37. ) -> bot.Switchbot:
  38. """Create a Bot device with mocked Bluetooth commands."""
  39. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "WoHand")
  40. device = bot.Switchbot(ble_device)
  41. device.update_from_advertisement(make_advertisement_data(ble_device))
  42. if inverse_direction is not None:
  43. device._update_parsed_data({"inverseDirection": inverse_direction})
  44. device._send_command = AsyncMock(return_value=b"\x01")
  45. device.update = AsyncMock()
  46. return device
  47. @pytest.mark.asyncio
  48. async def test_update_reads_inverse_direction() -> None:
  49. """Test updating reads inverse direction."""
  50. device = create_device_for_command_testing()
  51. device.update = bot.Switchbot.update.__get__(device)
  52. device._get_basic_info = AsyncMock(
  53. return_value=bytes([1, 89, 31, 75, 0, 0, 0, 0, 2, 17, 0])
  54. )
  55. await device.update()
  56. assert device.inverse_direction() is True
  57. assert device.parsed_data["strength"] == 75
  58. @pytest.mark.asyncio
  59. @pytest.mark.parametrize(
  60. ("inverse_direction", "expected_command"),
  61. [(False, "57034b10"), (True, "57034b11")],
  62. )
  63. async def test_set_inverse_direction_preserves_mode_and_strength(
  64. inverse_direction: bool,
  65. expected_command: str,
  66. ) -> None:
  67. """Test changing inverse direction preserves the current Bot settings."""
  68. device = create_device_for_command_testing()
  69. device.get_basic_info = AsyncMock(
  70. return_value={
  71. "battery": 89,
  72. "firmware": 3.1,
  73. "strength": 75,
  74. "timers": 2,
  75. "switchMode": True,
  76. "inverseDirection": False,
  77. "holdSeconds": 0,
  78. }
  79. )
  80. assert await device.set_inverse_direction(inverse_direction) is True
  81. device._send_command.assert_awaited_once_with(expected_command)
  82. @pytest.mark.asyncio
  83. async def test_set_inverse_direction_raises_when_settings_unavailable() -> None:
  84. """Test changing inverse direction fails if current settings cannot be read."""
  85. device = create_device_for_command_testing()
  86. device.get_basic_info = AsyncMock(return_value=None)
  87. with pytest.raises(
  88. SwitchbotOperationError, match="Unable to get current Bot mode settings"
  89. ):
  90. await device.set_inverse_direction(True)
  91. @pytest.mark.asyncio
  92. async def test_set_inverse_direction_returns_false_for_rejected_command() -> None:
  93. """Test a rejected inverse direction command does not update the cache."""
  94. device = create_device_for_command_testing(inverse_direction=False)
  95. device.get_basic_info = AsyncMock(
  96. return_value={
  97. "switchMode": True,
  98. "strength": 75,
  99. }
  100. )
  101. device._send_command = AsyncMock(return_value=b"\x00")
  102. assert await device.set_inverse_direction(True) is False
  103. assert device.inverse_direction() is False
  104. @pytest.mark.asyncio
  105. async def test_set_switch_mode_uses_confirmed_settings() -> None:
  106. """Test setting switch mode caches the settings read back from the Bot."""
  107. device = create_device_for_command_testing(inverse_direction=False)
  108. device.update = bot.Switchbot.update.__get__(device)
  109. device._send_command = AsyncMock(
  110. side_effect=[
  111. b"\x01",
  112. bytes([1, 89, 31, 75, 0, 0, 0, 0, 2, 17, 0]),
  113. ]
  114. )
  115. callback = Mock()
  116. device.subscribe(callback)
  117. assert await device.set_switch_mode(
  118. switch_mode=True,
  119. strength=75,
  120. inverse=True,
  121. )
  122. assert device.switch_mode() is True
  123. assert device.inverse_direction() is True
  124. assert device.parsed_data["strength"] == 75
  125. assert device._send_command.await_args_list[0].args == ("57034b11",)
  126. callback.assert_called_once_with()