test_bulb.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. from unittest.mock import AsyncMock, MagicMock
  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 bulb
  7. from switchbot.devices.device import SwitchbotOperationError
  8. from .test_adv_parser import generate_ble_device
  9. def create_device_for_command_testing(
  10. init_data: dict | None = None, model: SwitchbotModel = SwitchbotModel.COLOR_BULB
  11. ):
  12. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  13. device = bulb.SwitchbotBulb(ble_device, model=model)
  14. device.update_from_advertisement(make_advertisement_data(ble_device, init_data))
  15. device._send_command = AsyncMock()
  16. device._check_command_result = MagicMock()
  17. device.update = AsyncMock()
  18. return device
  19. def make_advertisement_data(ble_device: BLEDevice, init_data: dict | None = None):
  20. """Set advertisement data with defaults."""
  21. if init_data is None:
  22. init_data = {}
  23. return SwitchBotAdvertisement(
  24. address="aa:bb:cc:dd:ee:ff",
  25. data={
  26. "rawAdvData": b"u\x00d",
  27. "data": {
  28. "brightness": 1,
  29. "color_mode": 2,
  30. "delay": False,
  31. "isOn": True,
  32. "loop_index": 0,
  33. "preset": False,
  34. "sequence_number": 2,
  35. "speed": 0,
  36. }
  37. | init_data,
  38. "isEncrypted": False,
  39. "model": "u",
  40. "modelFriendlyName": "Color Bulb",
  41. "modelName": SwitchbotModel.COLOR_BULB,
  42. },
  43. device=ble_device,
  44. rssi=-80,
  45. active=True,
  46. )
  47. @pytest.mark.asyncio
  48. async def test_default_info():
  49. """Test default initialization of the color bulb."""
  50. device = create_device_for_command_testing()
  51. assert device.rgb is None
  52. device._state = {"r": 30, "g": 0, "b": 0, "cw": 3200}
  53. assert device.is_on() is True
  54. assert device.on is True
  55. assert device.color_mode == ColorMode.RGB
  56. assert device.color_modes == {ColorMode.RGB, ColorMode.COLOR_TEMP}
  57. assert device.rgb == (30, 0, 0)
  58. assert device.color_temp == 3200
  59. assert device.brightness == 1
  60. assert device.min_temp == 2700
  61. assert device.max_temp == 6500
  62. assert device.get_effect_list == ["colorful", "flickering", "breathing"]
  63. @pytest.mark.asyncio
  64. @pytest.mark.parametrize(
  65. ("basic_info", "version_info"), [(True, False), (False, True), (False, False)]
  66. )
  67. async def test_get_basic_info_returns_none(basic_info, version_info):
  68. device = create_device_for_command_testing()
  69. device._send_command = AsyncMock(side_effect=[version_info, basic_info])
  70. device._check_command_result = MagicMock(
  71. side_effect=[bool(version_info), bool(basic_info)]
  72. )
  73. assert await device.get_basic_info() is None
  74. @pytest.mark.asyncio
  75. @pytest.mark.parametrize(
  76. ("info_data", "result"),
  77. [
  78. (
  79. {
  80. "basic_info": b"\x01\x80\x01\xff\x91\x96\x00\x00\xff\xff\x02",
  81. "version_info": b"\x01\x01\x11",
  82. },
  83. [True, 1, 255, 145, 150, 0, 2, 1.7],
  84. ),
  85. (
  86. {
  87. "basic_info": b"\x01\x80;\x00\x00\x00\x0c\x99\xff\xff\x01",
  88. "version_info": b"\x01\x01\x11",
  89. },
  90. [True, 59, 0, 0, 0, 3225, 1, 1.7],
  91. ),
  92. (
  93. {
  94. "basic_info": b"\x01\x80\t!7\xff\x00\x00\xff\xff\x02",
  95. "version_info": b"\x01\x01\x11",
  96. },
  97. [True, 9, 33, 55, 255, 0, 2, 1.7],
  98. ),
  99. ],
  100. )
  101. async def test_get_basic_info(info_data, result):
  102. """Test getting basic info from the color bulb."""
  103. device = create_device_for_command_testing()
  104. device._send_command = AsyncMock(
  105. side_effect=[info_data["version_info"], info_data["basic_info"]]
  106. )
  107. device._check_command_result = MagicMock(side_effect=[True, True])
  108. info = await device.get_basic_info()
  109. assert info["isOn"] is result[0]
  110. assert info["brightness"] == result[1]
  111. assert info["r"] == result[2]
  112. assert info["g"] == result[3]
  113. assert info["b"] == result[4]
  114. assert info["cw"] == result[5]
  115. assert info["color_mode"] == result[6]
  116. assert info["firmware"] == result[7]
  117. @pytest.mark.asyncio
  118. async def test_set_color_temp():
  119. """Test setting color temperature."""
  120. device = create_device_for_command_testing()
  121. await device.set_color_temp(50, 3000)
  122. device._send_command.assert_called_with(
  123. device._set_color_temp_command.format("320BB8")
  124. )
  125. @pytest.mark.asyncio
  126. async def test_turn_on():
  127. """Test turning on the color bulb."""
  128. device = create_device_for_command_testing({"isOn": True})
  129. await device.turn_on()
  130. device._send_command.assert_called_with(device._turn_on_command)
  131. assert device.is_on() is True
  132. @pytest.mark.asyncio
  133. async def test_turn_off():
  134. """Test turning off the color bulb."""
  135. device = create_device_for_command_testing({"isOn": False})
  136. await device.turn_off()
  137. device._send_command.assert_called_with(device._turn_off_command)
  138. assert device.is_on() is False
  139. @pytest.mark.asyncio
  140. async def test_set_brightness():
  141. """Test setting brightness."""
  142. device = create_device_for_command_testing()
  143. await device.set_brightness(75)
  144. device._send_command.assert_called_with(device._set_brightness_command.format("4B"))
  145. @pytest.mark.asyncio
  146. async def test_set_rgb():
  147. """Test setting RGB values."""
  148. device = create_device_for_command_testing()
  149. await device.set_rgb(100, 255, 128, 64)
  150. device._send_command.assert_called_with(device._set_rgb_command.format("64FF8040"))
  151. @pytest.mark.asyncio
  152. async def test_set_effect_with_invalid_effect():
  153. """Test setting an invalid effect."""
  154. device = create_device_for_command_testing()
  155. with pytest.raises(
  156. SwitchbotOperationError, match="Effect invalid_effect not supported"
  157. ):
  158. await device.set_effect("invalid_effect")
  159. @pytest.mark.asyncio
  160. async def test_set_effect_with_valid_effect():
  161. """Test setting a valid effect."""
  162. device = create_device_for_command_testing()
  163. await device.set_effect("colorful")
  164. device._send_command.assert_called_with(device._effect_dict["colorful"][0])
  165. assert device.get_effect() == "colorful"
  166. def test_effect_list_contains_lowercase_names():
  167. """Test that all effect names in get_effect_list are lowercase."""
  168. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  169. device = bulb.SwitchbotBulb(ble_device)
  170. effect_list = device.get_effect_list
  171. assert effect_list is not None, "Effect list should not be None"
  172. assert effect_list == ["colorful", "flickering", "breathing"]
  173. for effect_name in effect_list:
  174. assert effect_name.islower(), f"Effect name '{effect_name}' is not lowercase"
  175. @pytest.mark.asyncio
  176. async def test_set_effect_normalizes_case():
  177. """Test that set_effect normalizes effect names to lowercase."""
  178. device = create_device_for_command_testing()
  179. # Test various case combinations
  180. test_cases = ["COLORFUL", "Colorful", "CoLoRfUl", "colorful"]
  181. for test_effect in test_cases:
  182. await device.set_effect(test_effect)
  183. # Should always work regardless of case
  184. device._send_command.assert_called()
  185. assert device.get_effect() == test_effect # Stored as provided