test_strip_light.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  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. # Check that effect list contains expected lowercase effect names
  68. effect_list = device.get_effect_list
  69. assert effect_list is not None
  70. assert all(effect.islower() for effect in effect_list)
  71. # Verify some known effects are present
  72. assert "christmas" in effect_list
  73. assert "halloween" in effect_list
  74. assert "sunset" in effect_list
  75. @pytest.mark.asyncio
  76. @pytest.mark.parametrize(
  77. ("basic_info", "version_info"), [(True, False), (False, True), (False, False)]
  78. )
  79. async def test_get_basic_info_returns_none(basic_info, version_info):
  80. device = create_device_for_command_testing()
  81. async def mock_get_basic_info(arg):
  82. if arg == device._get_basic_info_command[1]:
  83. return basic_info
  84. if arg == device._get_basic_info_command[0]:
  85. return version_info
  86. return None
  87. device._get_basic_info = AsyncMock(side_effect=mock_get_basic_info)
  88. assert await device.get_basic_info() is None
  89. @pytest.mark.asyncio
  90. @pytest.mark.parametrize(
  91. ("info_data", "result"),
  92. [
  93. (
  94. {
  95. "basic_info": b"\x01\x00<\xff\x00\xd8\x00\x19d\x00\x03",
  96. "version_info": b"\x01\x01\n",
  97. },
  98. [False, 60, 255, 0, 216, 6500, 3, 1.0],
  99. ),
  100. (
  101. {
  102. "basic_info": b"\x01\x80NK\xff:\x00\x19d\xff\x02",
  103. "version_info": b"\x01\x01\n",
  104. },
  105. [True, 78, 75, 255, 58, 6500, 2, 1.0],
  106. ),
  107. (
  108. {
  109. "basic_info": b"\x01\x80$K\xff:\x00\x13\xf9\xff\x06",
  110. "version_info": b"\x01\x01\n",
  111. },
  112. [True, 36, 75, 255, 58, 5113, 6, 1.0],
  113. ),
  114. ],
  115. )
  116. async def test_strip_light_get_basic_info(info_data, result):
  117. """Test getting basic info from the strip light."""
  118. device = create_device_for_command_testing()
  119. async def mock_get_basic_info(args: str) -> list[int] | None:
  120. if args == device._get_basic_info_command[1]:
  121. return info_data["basic_info"]
  122. if args == device._get_basic_info_command[0]:
  123. return info_data["version_info"]
  124. return None
  125. device._get_basic_info = AsyncMock(side_effect=mock_get_basic_info)
  126. info = await device.get_basic_info()
  127. assert info["isOn"] is result[0]
  128. assert info["brightness"] == result[1]
  129. assert info["r"] == result[2]
  130. assert info["g"] == result[3]
  131. assert info["b"] == result[4]
  132. assert info["cw"] == result[5]
  133. assert info["color_mode"] == result[6]
  134. assert info["firmware"] == result[7]
  135. @pytest.mark.asyncio
  136. async def test_set_color_temp():
  137. """Test setting color temperature."""
  138. device = create_device_for_command_testing()
  139. await device.set_color_temp(50, 3000)
  140. device._send_command.assert_called_with(
  141. device._set_color_temp_command.format("320BB8")
  142. )
  143. @pytest.mark.asyncio
  144. async def test_turn_on():
  145. """Test turning on the strip light."""
  146. device = create_device_for_command_testing({"isOn": True})
  147. await device.turn_on()
  148. device._send_command.assert_called_with(device._turn_on_command)
  149. assert device.is_on() is True
  150. @pytest.mark.asyncio
  151. async def test_turn_off():
  152. """Test turning off the strip light."""
  153. device = create_device_for_command_testing({"isOn": False})
  154. await device.turn_off()
  155. device._send_command.assert_called_with(device._turn_off_command)
  156. assert device.is_on() is False
  157. @pytest.mark.asyncio
  158. async def test_set_brightness():
  159. """Test setting brightness."""
  160. device = create_device_for_command_testing()
  161. await device.set_brightness(75)
  162. device._send_command.assert_called_with(device._set_brightness_command.format("4B"))
  163. @pytest.mark.asyncio
  164. async def test_set_rgb():
  165. """Test setting RGB values."""
  166. device = create_device_for_command_testing()
  167. await device.set_rgb(100, 255, 128, 64)
  168. device._send_command.assert_called_with(device._set_rgb_command.format("64FF8040"))
  169. @pytest.mark.asyncio
  170. async def test_set_effect_with_invalid_effect():
  171. """Test setting an invalid effect."""
  172. device = create_device_for_command_testing()
  173. with pytest.raises(
  174. SwitchbotOperationError, match="Effect invalid_effect not supported"
  175. ):
  176. await device.set_effect("invalid_effect")
  177. @pytest.mark.asyncio
  178. async def test_set_effect_with_valid_effect():
  179. """Test setting a valid effect."""
  180. device = create_device_for_command_testing()
  181. device._send_multiple_commands = AsyncMock()
  182. await device.set_effect("christmas")
  183. device._send_multiple_commands.assert_called_with(device._effect_dict["christmas"])
  184. assert device.get_effect() == "christmas"
  185. def test_effect_list_contains_lowercase_names():
  186. """Test that all effect names in get_effect_list are lowercase."""
  187. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  188. device = light_strip.SwitchbotLightStrip(ble_device)
  189. effect_list = device.get_effect_list
  190. assert effect_list is not None, "Effect list should not be None"
  191. # All effect names should be lowercase
  192. for effect_name in effect_list:
  193. assert effect_name.islower(), f"Effect name '{effect_name}' is not lowercase"
  194. # Verify some known effects are present
  195. assert "christmas" in effect_list
  196. assert "halloween" in effect_list
  197. assert "sunset" in effect_list
  198. @pytest.mark.asyncio
  199. async def test_set_effect_normalizes_case():
  200. """Test that set_effect normalizes effect names to lowercase."""
  201. device = create_device_for_command_testing()
  202. device._send_multiple_commands = AsyncMock()
  203. # Test various case combinations
  204. test_cases = ["CHRISTMAS", "Christmas", "ChRiStMaS", "christmas"]
  205. for test_effect in test_cases:
  206. await device.set_effect(test_effect)
  207. # Should always work regardless of case
  208. device._send_multiple_commands.assert_called()
  209. assert device.get_effect() == test_effect # Stored as provided
  210. @pytest.mark.asyncio
  211. @patch.object(SwitchbotEncryptedDevice, "verify_encryption_key", new_callable=AsyncMock)
  212. async def test_verify_encryption_key(mock_parent_verify):
  213. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  214. key_id = "ff"
  215. encryption_key = "ffffffffffffffffffffffffffffffff"
  216. mock_parent_verify.return_value = True
  217. result = await light_strip.SwitchbotStripLight3.verify_encryption_key(
  218. device=ble_device,
  219. key_id=key_id,
  220. encryption_key=encryption_key,
  221. )
  222. mock_parent_verify.assert_awaited_once_with(
  223. ble_device,
  224. key_id,
  225. encryption_key,
  226. SwitchbotModel.STRIP_LIGHT_3,
  227. )
  228. assert result is True
  229. def create_strip_light_device(init_data: dict | None = None):
  230. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  231. return light_strip.SwitchbotLightStrip(ble_device)
  232. @pytest.mark.asyncio
  233. async def test_strip_light_supported_color_modes():
  234. """Test that the strip light supports the expected color modes."""
  235. device = create_strip_light_device()
  236. assert device.color_modes == {
  237. ColorMode.RGB,
  238. }
  239. @pytest.mark.asyncio
  240. @pytest.mark.parametrize(
  241. ("commands", "results", "final_result"),
  242. [
  243. (("command1", "command2"), [(b"\x01", False), (None, False)], False),
  244. (("command1", "command2"), [(None, False), (b"\x01", True)], True),
  245. (("command1", "command2"), [(b"\x01", True), (b"\x01", False)], True),
  246. ],
  247. )
  248. async def test_send_multiple_commands(commands, results, final_result):
  249. """Test sending multiple commands."""
  250. device = create_device_for_command_testing()
  251. device._send_command = AsyncMock(side_effect=[r[0] for r in results])
  252. device._check_command_result = MagicMock(side_effect=[r[1] for r in results])
  253. result = await device._send_multiple_commands(list(commands))
  254. assert result is final_result
  255. @pytest.mark.asyncio
  256. async def test_unimplemented_color_mode():
  257. class TestDevice(SwitchbotBaseLight):
  258. pass
  259. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  260. device = TestDevice(ble_device)
  261. with pytest.raises(NotImplementedError):
  262. _ = device.color_mode
  263. @pytest.mark.asyncio
  264. async def test_exception_with_wrong_model():
  265. class TestDevice(SwitchbotBaseLight):
  266. def __init__(self, device: BLEDevice, model: str = "unknown") -> None:
  267. super().__init__(device, model=model)
  268. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  269. device = TestDevice(ble_device)
  270. with pytest.raises(
  271. SwitchbotOperationError,
  272. match="Current device aa:bb:cc:dd:ee:ff does not support this functionality",
  273. ):
  274. await device.set_rgb(100, 255, 128, 64)