test_strip_light.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. from unittest.mock import AsyncMock, MagicMock, patch
  2. import pytest
  3. from bleak.backends.device import BLEDevice
  4. from switchbot import SwitchBotAdvertisement, SwitchbotModel
  5. from switchbot.const.light import ColorMode
  6. from switchbot.devices import light_strip
  7. from switchbot.devices.base_light import SwitchbotBaseLight
  8. from switchbot.devices.device import SwitchbotEncryptedDevice, SwitchbotOperationError
  9. from .test_adv_parser import generate_ble_device
  10. def create_device_for_command_testing(
  11. init_data: dict | None = None, model: SwitchbotModel = SwitchbotModel.STRIP_LIGHT_3
  12. ):
  13. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  14. device = light_strip.SwitchbotStripLight3(
  15. ble_device, "ff", "ffffffffffffffffffffffffffffffff", model=model
  16. )
  17. device.update_from_advertisement(make_advertisement_data(ble_device, init_data))
  18. device._send_command = AsyncMock()
  19. device._check_command_result = MagicMock()
  20. device.update = AsyncMock()
  21. return device
  22. def make_advertisement_data(ble_device: BLEDevice, init_data: dict | None = None):
  23. """Set advertisement data with defaults."""
  24. if init_data is None:
  25. init_data = {}
  26. return SwitchBotAdvertisement(
  27. address="aa:bb:cc:dd:ee:ff",
  28. data={
  29. "rawAdvData": b"\x00\x00\x00\x00\x10\xd0\xb1",
  30. "data": {
  31. "sequence_number": 133,
  32. "isOn": True,
  33. "brightness": 30,
  34. "delay": False,
  35. "network_state": 2,
  36. "color_mode": 2,
  37. "cw": 4753,
  38. }
  39. | init_data,
  40. "isEncrypted": False,
  41. "model": b"\x00\x10\xd0\xb1",
  42. "modelFriendlyName": "Strip Light 3",
  43. "modelName": SwitchbotModel.STRIP_LIGHT_3,
  44. },
  45. device=ble_device,
  46. rssi=-80,
  47. active=True,
  48. )
  49. @pytest.mark.asyncio
  50. async def test_default_info():
  51. """Test default initialization of the strip light."""
  52. device = create_device_for_command_testing()
  53. assert device.rgb is None
  54. device._state = {"r": 30, "g": 0, "b": 0, "cw": 3200}
  55. assert device.is_on() is True
  56. assert device.on is True
  57. assert device.color_mode == ColorMode.RGB
  58. assert device.color_modes == {
  59. ColorMode.RGB,
  60. ColorMode.COLOR_TEMP,
  61. }
  62. assert device.rgb == (30, 0, 0)
  63. assert device.color_temp == 3200
  64. assert device.brightness == 30
  65. assert device.min_temp == 2700
  66. assert device.max_temp == 6500
  67. assert device.get_effect_list == list(device._effect_dict.keys())
  68. @pytest.mark.asyncio
  69. @pytest.mark.parametrize(
  70. ("basic_info", "version_info"), [(True, False), (False, True), (False, False)]
  71. )
  72. async def test_get_basic_info_returns_none(basic_info, version_info):
  73. device = create_device_for_command_testing()
  74. async def mock_get_basic_info(arg):
  75. if arg == device._get_basic_info_command[1]:
  76. return basic_info
  77. if arg == device._get_basic_info_command[0]:
  78. return version_info
  79. return None
  80. device._get_basic_info = AsyncMock(side_effect=mock_get_basic_info)
  81. assert await device.get_basic_info() is None
  82. @pytest.mark.asyncio
  83. @pytest.mark.parametrize(
  84. ("info_data", "result"),
  85. [
  86. (
  87. {
  88. "basic_info": b"\x01\x00<\xff\x00\xd8\x00\x19d\x00\x03",
  89. "version_info": b"\x01\x01\n",
  90. },
  91. [False, 60, 255, 0, 216, 6500, 3, 1.0],
  92. ),
  93. (
  94. {
  95. "basic_info": b"\x01\x80NK\xff:\x00\x19d\xff\x02",
  96. "version_info": b"\x01\x01\n",
  97. },
  98. [True, 78, 75, 255, 58, 6500, 2, 1.0],
  99. ),
  100. (
  101. {
  102. "basic_info": b"\x01\x80$K\xff:\x00\x13\xf9\xff\x06",
  103. "version_info": b"\x01\x01\n",
  104. },
  105. [True, 36, 75, 255, 58, 5113, 6, 1.0],
  106. ),
  107. ],
  108. )
  109. async def test_strip_light_get_basic_info(info_data, result):
  110. """Test getting basic info from the strip light."""
  111. device = create_device_for_command_testing()
  112. async def mock_get_basic_info(args: str) -> list[int] | None:
  113. if args == device._get_basic_info_command[1]:
  114. return info_data["basic_info"]
  115. if args == device._get_basic_info_command[0]:
  116. return info_data["version_info"]
  117. return None
  118. device._get_basic_info = AsyncMock(side_effect=mock_get_basic_info)
  119. info = await device.get_basic_info()
  120. assert info["isOn"] is result[0]
  121. assert info["brightness"] == result[1]
  122. assert info["r"] == result[2]
  123. assert info["g"] == result[3]
  124. assert info["b"] == result[4]
  125. assert info["cw"] == result[5]
  126. assert info["color_mode"] == result[6]
  127. assert info["firmware"] == result[7]
  128. @pytest.mark.asyncio
  129. async def test_set_color_temp():
  130. """Test setting color temperature."""
  131. device = create_device_for_command_testing()
  132. await device.set_color_temp(50, 3000)
  133. device._send_command.assert_called_with(
  134. device._set_color_temp_command.format("320BB8")
  135. )
  136. @pytest.mark.asyncio
  137. async def test_turn_on():
  138. """Test turning on the strip light."""
  139. device = create_device_for_command_testing({"isOn": True})
  140. await device.turn_on()
  141. device._send_command.assert_called_with(device._turn_on_command)
  142. assert device.is_on() is True
  143. @pytest.mark.asyncio
  144. async def test_turn_off():
  145. """Test turning off the strip light."""
  146. device = create_device_for_command_testing({"isOn": False})
  147. await device.turn_off()
  148. device._send_command.assert_called_with(device._turn_off_command)
  149. assert device.is_on() is False
  150. @pytest.mark.asyncio
  151. async def test_set_brightness():
  152. """Test setting brightness."""
  153. device = create_device_for_command_testing()
  154. await device.set_brightness(75)
  155. device._send_command.assert_called_with(device._set_brightness_command.format("4B"))
  156. @pytest.mark.asyncio
  157. async def test_set_rgb():
  158. """Test setting RGB values."""
  159. device = create_device_for_command_testing()
  160. await device.set_rgb(100, 255, 128, 64)
  161. device._send_command.assert_called_with(device._set_rgb_command.format("64FF8040"))
  162. @pytest.mark.asyncio
  163. async def test_set_effect_with_invalid_effect():
  164. """Test setting an invalid effect."""
  165. device = create_device_for_command_testing()
  166. with pytest.raises(
  167. SwitchbotOperationError, match="Effect invalid_effect not supported"
  168. ):
  169. await device.set_effect("invalid_effect")
  170. @pytest.mark.asyncio
  171. async def test_set_effect_with_valid_effect():
  172. """Test setting a valid effect."""
  173. device = create_device_for_command_testing()
  174. device._send_multiple_commands = AsyncMock()
  175. await device.set_effect("Christmas")
  176. device._send_multiple_commands.assert_called_with(device._effect_dict["Christmas"])
  177. assert device.get_effect() == "Christmas"
  178. @pytest.mark.asyncio
  179. @patch.object(SwitchbotEncryptedDevice, "verify_encryption_key", new_callable=AsyncMock)
  180. async def test_verify_encryption_key(mock_parent_verify):
  181. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  182. key_id = "ff"
  183. encryption_key = "ffffffffffffffffffffffffffffffff"
  184. mock_parent_verify.return_value = True
  185. result = await light_strip.SwitchbotStripLight3.verify_encryption_key(
  186. device=ble_device,
  187. key_id=key_id,
  188. encryption_key=encryption_key,
  189. )
  190. mock_parent_verify.assert_awaited_once_with(
  191. ble_device,
  192. key_id,
  193. encryption_key,
  194. SwitchbotModel.STRIP_LIGHT_3,
  195. )
  196. assert result is True
  197. def create_strip_light_device(init_data: dict | None = None):
  198. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  199. return light_strip.SwitchbotLightStrip(ble_device)
  200. @pytest.mark.asyncio
  201. async def test_strip_light_supported_color_modes():
  202. """Test that the strip light supports the expected color modes."""
  203. device = create_strip_light_device()
  204. assert device.color_modes == {
  205. ColorMode.RGB,
  206. }
  207. @pytest.mark.asyncio
  208. @pytest.mark.parametrize(
  209. ("commands", "results", "final_result"),
  210. [
  211. (("command1", "command2"), [(b"\x01", False), (None, False)], False),
  212. (("command1", "command2"), [(None, False), (b"\x01", True)], True),
  213. (("command1", "command2"), [(b"\x01", True), (b"\x01", False)], True),
  214. ],
  215. )
  216. async def test_send_multiple_commands(commands, results, final_result):
  217. """Test sending multiple commands."""
  218. device = create_device_for_command_testing()
  219. device._send_command = AsyncMock(side_effect=[r[0] for r in results])
  220. device._check_command_result = MagicMock(side_effect=[r[1] for r in results])
  221. result = await device._send_multiple_commands(list(commands))
  222. assert result is final_result
  223. @pytest.mark.asyncio
  224. async def test_unimplemented_color_mode():
  225. class TestDevice(SwitchbotBaseLight):
  226. pass
  227. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  228. device = TestDevice(ble_device)
  229. with pytest.raises(NotImplementedError):
  230. _ = device.color_mode
  231. @pytest.mark.asyncio
  232. async def test_exception_with_wrong_model():
  233. class TestDevice(SwitchbotBaseLight):
  234. def __init__(self, device: BLEDevice, model: str = "unknown") -> None:
  235. super().__init__(device, model=model)
  236. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  237. device = TestDevice(ble_device)
  238. with pytest.raises(
  239. SwitchbotOperationError,
  240. match="Current device aa:bb:cc:dd:ee:ff does not support this functionality",
  241. ):
  242. await device.set_rgb(100, 255, 128, 64)