test_curtain.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. from unittest.mock import AsyncMock, Mock
  2. import pytest
  3. from bleak.backends.device import BLEDevice
  4. from switchbot import SwitchBotAdvertisement, SwitchbotModel
  5. from switchbot.devices import curtain
  6. from switchbot.devices.base_cover import COVER_EXT_SUM_KEY
  7. from .test_adv_parser import generate_ble_device
  8. def create_device_for_command_testing(calibration=True, reverse_mode=False):
  9. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  10. curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode)
  11. curtain_device.update_from_advertisement(
  12. make_advertisement_data(ble_device, True, 50, calibration)
  13. )
  14. curtain_device._send_multiple_commands = AsyncMock()
  15. curtain_device.update = AsyncMock()
  16. return curtain_device
  17. def make_advertisement_data(
  18. ble_device: BLEDevice, in_motion: bool, position: int, calibration: bool = True
  19. ):
  20. """Set advertisement data with defaults."""
  21. return SwitchBotAdvertisement(
  22. address="aa:bb:cc:dd:ee:ff",
  23. data={
  24. "rawAdvData": b"c\xc0X\x00\x11\x04",
  25. "data": {
  26. "calibration": calibration,
  27. "battery": 88,
  28. "inMotion": in_motion,
  29. "position": position,
  30. "lightLevel": 1,
  31. "deviceChain": 1,
  32. },
  33. "isEncrypted": False,
  34. "model": "c",
  35. "modelFriendlyName": "Curtain",
  36. "modelName": SwitchbotModel.CURTAIN,
  37. },
  38. device=ble_device,
  39. rssi=-80,
  40. active=True,
  41. )
  42. @pytest.mark.parametrize("reverse_mode", [(True), (False)])
  43. def test_device_passive_not_in_motion(reverse_mode):
  44. """Test passive not in motion advertisement."""
  45. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  46. curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode)
  47. curtain_device.update_from_advertisement(
  48. make_advertisement_data(ble_device, False, 0)
  49. )
  50. assert curtain_device.is_opening() is False
  51. assert curtain_device.is_closing() is False
  52. @pytest.mark.parametrize("reverse_mode", [(True), (False)])
  53. def test_device_passive_opening(reverse_mode):
  54. """Test passive opening advertisement."""
  55. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  56. curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode)
  57. curtain_device.update_from_advertisement(
  58. make_advertisement_data(ble_device, True, 0)
  59. )
  60. curtain_device.update_from_advertisement(
  61. make_advertisement_data(ble_device, True, 10)
  62. )
  63. assert curtain_device.is_opening() is True
  64. assert curtain_device.is_closing() is False
  65. @pytest.mark.parametrize("reverse_mode", [(True), (False)])
  66. def test_device_passive_closing(reverse_mode):
  67. """Test passive closing advertisement."""
  68. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  69. curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode)
  70. curtain_device.update_from_advertisement(
  71. make_advertisement_data(ble_device, True, 100)
  72. )
  73. curtain_device.update_from_advertisement(
  74. make_advertisement_data(ble_device, True, 90)
  75. )
  76. assert curtain_device.is_opening() is False
  77. assert curtain_device.is_closing() is True
  78. @pytest.mark.parametrize("reverse_mode", [(True), (False)])
  79. def test_device_passive_opening_then_stop(reverse_mode):
  80. """Test passive stopped after opening advertisement."""
  81. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  82. curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode)
  83. curtain_device.update_from_advertisement(
  84. make_advertisement_data(ble_device, True, 0)
  85. )
  86. curtain_device.update_from_advertisement(
  87. make_advertisement_data(ble_device, True, 10)
  88. )
  89. curtain_device.update_from_advertisement(
  90. make_advertisement_data(ble_device, False, 10)
  91. )
  92. assert curtain_device.is_opening() is False
  93. assert curtain_device.is_closing() is False
  94. @pytest.mark.parametrize("reverse_mode", [(True), (False)])
  95. def test_device_passive_closing_then_stop(reverse_mode):
  96. """Test passive stopped after closing advertisement."""
  97. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  98. curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode)
  99. curtain_device.update_from_advertisement(
  100. make_advertisement_data(ble_device, True, 100)
  101. )
  102. curtain_device.update_from_advertisement(
  103. make_advertisement_data(ble_device, True, 90)
  104. )
  105. curtain_device.update_from_advertisement(
  106. make_advertisement_data(ble_device, False, 90)
  107. )
  108. assert curtain_device.is_opening() is False
  109. assert curtain_device.is_closing() is False
  110. @pytest.mark.asyncio
  111. @pytest.mark.parametrize("reverse_mode", [(True), (False)])
  112. async def test_device_active_not_in_motion(reverse_mode):
  113. """Test active not in motion."""
  114. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  115. curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode)
  116. curtain_device.update_from_advertisement(
  117. make_advertisement_data(ble_device, False, 0)
  118. )
  119. basic_info = bytes([0, 0, 0, 0, 0, 0, 100, 0])
  120. async def custom_implementation():
  121. return basic_info
  122. curtain_device._get_basic_info = Mock(side_effect=custom_implementation)
  123. await curtain_device.get_basic_info()
  124. assert curtain_device.is_opening() is False
  125. assert curtain_device.is_closing() is False
  126. @pytest.mark.asyncio
  127. @pytest.mark.parametrize("reverse_mode", [(True), (False)])
  128. async def test_device_active_opening(reverse_mode):
  129. """Test active opening."""
  130. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  131. curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode)
  132. curtain_device.update_from_advertisement(
  133. make_advertisement_data(ble_device, True, 0)
  134. )
  135. basic_info = bytes([0, 0, 0, 0, 0, 67, 10, 0])
  136. async def custom_implementation():
  137. return basic_info
  138. curtain_device._get_basic_info = Mock(side_effect=custom_implementation)
  139. await curtain_device.get_basic_info()
  140. assert curtain_device.is_opening() is True
  141. assert curtain_device.is_closing() is False
  142. @pytest.mark.asyncio
  143. @pytest.mark.parametrize("reverse_mode", [(True), (False)])
  144. async def test_device_active_closing(reverse_mode):
  145. """Test active closing."""
  146. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  147. curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode)
  148. curtain_device.update_from_advertisement(
  149. make_advertisement_data(ble_device, True, 100)
  150. )
  151. basic_info = bytes([0, 0, 0, 0, 0, 67, 90, 0])
  152. async def custom_implementation():
  153. return basic_info
  154. curtain_device._get_basic_info = Mock(side_effect=custom_implementation)
  155. await curtain_device.get_basic_info()
  156. assert curtain_device.is_opening() is False
  157. assert curtain_device.is_closing() is True
  158. @pytest.mark.asyncio
  159. @pytest.mark.parametrize("reverse_mode", [(True), (False)])
  160. async def test_device_active_opening_then_stop(reverse_mode):
  161. """Test active stopped after opening."""
  162. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  163. curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode)
  164. curtain_device.update_from_advertisement(
  165. make_advertisement_data(ble_device, True, 0)
  166. )
  167. basic_info = bytes([0, 0, 0, 0, 0, 67, 10, 0])
  168. async def custom_implementation():
  169. return basic_info
  170. curtain_device._get_basic_info = Mock(side_effect=custom_implementation)
  171. await curtain_device.get_basic_info()
  172. basic_info = bytes([0, 0, 0, 0, 0, 0, 10, 0])
  173. await curtain_device.get_basic_info()
  174. assert curtain_device.is_opening() is False
  175. assert curtain_device.is_closing() is False
  176. @pytest.mark.asyncio
  177. @pytest.mark.parametrize("reverse_mode", [(True), (False)])
  178. async def test_device_active_closing_then_stop(reverse_mode):
  179. """Test active stopped after closing."""
  180. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  181. curtain_device = curtain.SwitchbotCurtain(ble_device, reverse_mode=reverse_mode)
  182. curtain_device.update_from_advertisement(
  183. make_advertisement_data(ble_device, True, 100)
  184. )
  185. basic_info = bytes([0, 0, 0, 0, 0, 67, 90, 0])
  186. async def custom_implementation():
  187. return basic_info
  188. curtain_device._get_basic_info = Mock(side_effect=custom_implementation)
  189. await curtain_device.get_basic_info()
  190. basic_info = bytes([0, 0, 0, 0, 0, 0, 90, 0])
  191. await curtain_device.get_basic_info()
  192. assert curtain_device.is_opening() is False
  193. assert curtain_device.is_closing() is False
  194. @pytest.mark.asyncio
  195. async def test_get_basic_info_returns_none_when_no_data():
  196. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  197. curtain_device = curtain.SwitchbotCurtain(ble_device)
  198. curtain_device.update_from_advertisement(
  199. make_advertisement_data(ble_device, True, 0)
  200. )
  201. curtain_device._get_basic_info = AsyncMock(return_value=None)
  202. assert await curtain_device.get_basic_info() is None
  203. @pytest.mark.asyncio
  204. @pytest.mark.parametrize(
  205. "data,result",
  206. [
  207. (
  208. bytes([0, 1, 10, 2, 255, 255, 50, 4]),
  209. [1, 1, 2, "right_to_left", 1, 1, 50, 4],
  210. ),
  211. (bytes([0, 1, 10, 2, 0, 0, 50, 4]), [1, 1, 2, "left_to_right", 0, 0, 50, 4]),
  212. ],
  213. )
  214. async def test_get_basic_info(data, result):
  215. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  216. curtain_device = curtain.SwitchbotCurtain(ble_device)
  217. curtain_device.update_from_advertisement(
  218. make_advertisement_data(ble_device, True, 0)
  219. )
  220. async def custom_implementation():
  221. return data
  222. curtain_device._get_basic_info = Mock(side_effect=custom_implementation)
  223. info = await curtain_device.get_basic_info()
  224. assert info["battery"] == result[0]
  225. assert info["firmware"] == result[1]
  226. assert info["chainLength"] == result[2]
  227. assert info["openDirection"] == result[3]
  228. assert info["touchToOpen"] == result[4]
  229. assert info["light"] == result[4]
  230. assert info["fault"] == result[4]
  231. assert info["solarPanel"] == result[5]
  232. assert info["calibration"] == result[5]
  233. assert info["calibrated"] == result[5]
  234. assert info["inMotion"] == result[5]
  235. assert info["position"] == result[6]
  236. assert info["timers"] == result[7]
  237. @pytest.mark.asyncio
  238. async def test_open():
  239. curtain_device = create_device_for_command_testing()
  240. await curtain_device.open()
  241. assert curtain_device.is_opening() is True
  242. assert curtain_device.is_closing() is False
  243. curtain_device._send_multiple_commands.assert_awaited_once()
  244. @pytest.mark.asyncio
  245. async def test_close():
  246. curtain_device = create_device_for_command_testing()
  247. await curtain_device.close()
  248. assert curtain_device.is_opening() is False
  249. assert curtain_device.is_closing() is True
  250. curtain_device._send_multiple_commands.assert_awaited_once()
  251. @pytest.mark.asyncio
  252. async def test_stop():
  253. curtain_device = create_device_for_command_testing()
  254. await curtain_device.stop()
  255. assert curtain_device.is_opening() is False
  256. assert curtain_device.is_closing() is False
  257. curtain_device._send_multiple_commands.assert_awaited_once()
  258. @pytest.mark.asyncio
  259. async def test_set_position_opening():
  260. curtain_device = create_device_for_command_testing()
  261. await curtain_device.set_position(100)
  262. assert curtain_device.is_opening() is True
  263. assert curtain_device.is_closing() is False
  264. curtain_device._send_multiple_commands.assert_awaited_once()
  265. @pytest.mark.asyncio
  266. async def test_set_position_closing():
  267. curtain_device = create_device_for_command_testing()
  268. await curtain_device.set_position(0)
  269. assert curtain_device.is_opening() is False
  270. assert curtain_device.is_closing() is True
  271. curtain_device._send_multiple_commands.assert_awaited_once()
  272. def test_get_position():
  273. curtain_device = create_device_for_command_testing()
  274. assert curtain_device.get_position() == 50
  275. @pytest.mark.asyncio
  276. async def test_get_extended_info_summary_sends_command():
  277. curtain_device = create_device_for_command_testing()
  278. curtain_device._send_command = AsyncMock()
  279. await curtain_device.get_extended_info_summary()
  280. curtain_device._send_command.assert_awaited_once_with(key=COVER_EXT_SUM_KEY)
  281. @pytest.mark.asyncio
  282. @pytest.mark.parametrize("data_value", [(None), (b"\x07"), (b"\x00")])
  283. async def test_get_extended_info_summary_returns_none_when_bad_data(data_value):
  284. curtain_device = create_device_for_command_testing()
  285. curtain_device._send_command = AsyncMock(return_value=data_value)
  286. assert await curtain_device.get_extended_info_summary() is None
  287. @pytest.mark.asyncio
  288. @pytest.mark.parametrize(
  289. "data,result",
  290. [
  291. ([0, 0, 0], [True, False, False, "right_to_left"]),
  292. ([255, 255, 0], [False, True, True, "left_to_right"]),
  293. ],
  294. )
  295. async def test_get_extended_info_summary_returns_device0(data, result):
  296. curtain_device = create_device_for_command_testing()
  297. curtain_device._send_command = AsyncMock(return_value=bytes(data))
  298. ext_result = await curtain_device.get_extended_info_summary()
  299. assert ext_result["device0"]["openDirectionDefault"] == result[0]
  300. assert ext_result["device0"]["touchToOpen"] == result[1]
  301. assert ext_result["device0"]["light"] == result[2]
  302. assert ext_result["device0"]["openDirection"] == result[3]
  303. assert "device1" not in ext_result
  304. @pytest.mark.asyncio
  305. @pytest.mark.parametrize(
  306. "data,result",
  307. [
  308. ([0, 0, 1], [True, False, False, "right_to_left"]),
  309. ([255, 255, 255], [False, True, True, "left_to_right"]),
  310. ],
  311. )
  312. async def test_get_extended_info_summary_returns_device1(data, result):
  313. curtain_device = create_device_for_command_testing()
  314. curtain_device._send_command = AsyncMock(return_value=bytes(data))
  315. ext_result = await curtain_device.get_extended_info_summary()
  316. assert ext_result["device1"]["openDirectionDefault"] == result[0]
  317. assert ext_result["device1"]["touchToOpen"] == result[1]
  318. assert ext_result["device1"]["light"] == result[2]
  319. assert ext_result["device1"]["openDirection"] == result[3]
  320. def test_get_light_level():
  321. curtain_device = create_device_for_command_testing()
  322. assert curtain_device.get_light_level() == 1
  323. @pytest.mark.parametrize("reverse_mode", [(True), (False)])
  324. def test_is_reversed(reverse_mode):
  325. curtain_device = create_device_for_command_testing(reverse_mode=reverse_mode)
  326. assert curtain_device.is_reversed() == reverse_mode
  327. @pytest.mark.parametrize("calibration", [(True), (False)])
  328. def test_is_calibrated(calibration):
  329. curtain_device = create_device_for_command_testing(calibration=calibration)
  330. assert curtain_device.is_calibrated() == calibration