test_lock.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809
  1. import logging
  2. from unittest.mock import AsyncMock, Mock, patch
  3. import pytest
  4. from switchbot import SwitchbotModel
  5. from switchbot.const.lock import LockStatus
  6. from switchbot.devices import lock
  7. from .test_adv_parser import generate_ble_device
  8. def create_device_for_command_testing(model: str):
  9. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  10. return lock.SwitchbotLock(
  11. ble_device, "ff", "ffffffffffffffffffffffffffffffff", model=model
  12. )
  13. @pytest.mark.parametrize(
  14. "model",
  15. [
  16. SwitchbotModel.LOCK,
  17. SwitchbotModel.LOCK_LITE,
  18. SwitchbotModel.LOCK_PRO,
  19. SwitchbotModel.LOCK_ULTRA,
  20. SwitchbotModel.LOCK_VISION,
  21. SwitchbotModel.LOCK_VISION_PRO,
  22. SwitchbotModel.LOCK_PRO_WIFI,
  23. ],
  24. )
  25. def test_lock_init(model: str):
  26. """Test the initialization of the lock device."""
  27. device = create_device_for_command_testing(model)
  28. assert device._model == model
  29. @pytest.mark.parametrize(
  30. "model",
  31. [
  32. SwitchbotModel.AIR_PURIFIER,
  33. ],
  34. )
  35. def test_lock_init_with_invalid_model(model: str):
  36. """Test that initializing with an invalid model raises ValueError."""
  37. with pytest.raises(
  38. ValueError, match="initializing SwitchbotLock with a non-lock model"
  39. ):
  40. create_device_for_command_testing(model)
  41. @pytest.mark.asyncio
  42. @pytest.mark.parametrize(
  43. "model",
  44. [
  45. SwitchbotModel.LOCK,
  46. SwitchbotModel.LOCK_LITE,
  47. SwitchbotModel.LOCK_PRO,
  48. SwitchbotModel.LOCK_ULTRA,
  49. SwitchbotModel.LOCK_VISION,
  50. SwitchbotModel.LOCK_VISION_PRO,
  51. SwitchbotModel.LOCK_PRO_WIFI,
  52. ],
  53. )
  54. async def test_verify_encryption_key(model: str):
  55. """Test verify_encryption_key method."""
  56. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  57. with patch("switchbot.devices.lock.super") as mock_super:
  58. mock_super().verify_encryption_key = AsyncMock(return_value=True)
  59. result = await lock.SwitchbotLock.verify_encryption_key(
  60. ble_device, "key_id", "encryption_key", model
  61. )
  62. assert result is True
  63. @pytest.mark.asyncio
  64. @pytest.mark.parametrize(
  65. ("model", "command"),
  66. [
  67. (SwitchbotModel.LOCK, b"W\x0fN\x01\x01\x10\x80"),
  68. (SwitchbotModel.LOCK_LITE, b"W\x0fN\x01\x01\x10\x81"),
  69. (SwitchbotModel.LOCK_PRO, b"W\x0fN\x01\x01\x10\x85"),
  70. (SwitchbotModel.LOCK_ULTRA, b"W\x0fN\x01\x01\x10\x86"),
  71. (SwitchbotModel.LOCK_VISION, b"W\x0fN\x01\x01\x00\x80"),
  72. (SwitchbotModel.LOCK_VISION_PRO, b"W\x0fN\x01\x01\x00\x80"),
  73. (SwitchbotModel.LOCK_PRO_WIFI, b"W\x0fN\x01\x01\x10\x82"),
  74. ],
  75. )
  76. async def test_lock(model: str, command: bytes):
  77. """Test lock method."""
  78. device = create_device_for_command_testing(model)
  79. device._get_adv_value = Mock(return_value=LockStatus.UNLOCKED)
  80. with (
  81. patch.object(
  82. device, "_send_command", return_value=b"\x01\x00"
  83. ) as mock_send_command,
  84. patch.object(device, "_enable_notifications", return_value=True),
  85. patch.object(device, "_get_basic_info", return_value=b"\x00\x64\x01"),
  86. ):
  87. result = await device.lock()
  88. assert result is True
  89. mock_send_command.assert_any_call(lock.COMMAND_LOCK[model])
  90. @pytest.mark.asyncio
  91. @pytest.mark.parametrize(
  92. ("model", "command"),
  93. [
  94. (SwitchbotModel.LOCK, b"W\x0fN\x01\x01\x10\x80"),
  95. (SwitchbotModel.LOCK_LITE, b"W\x0fN\x01\x01\x10\x81"),
  96. (SwitchbotModel.LOCK_PRO, b"W\x0fN\x01\x01\x10\x84"),
  97. (SwitchbotModel.LOCK_ULTRA, b"W\x0fN\x01\x01\x10\x83"),
  98. (SwitchbotModel.LOCK_VISION, b"W\x0fN\x01\x01\x00\x80"),
  99. (SwitchbotModel.LOCK_VISION_PRO, b"W\x0fN\x01\x01\x00\x80"),
  100. (SwitchbotModel.LOCK_PRO_WIFI, b"W\x0fN\x01\x01\x10\x81"),
  101. ],
  102. )
  103. async def test_unlock(model: str, command: bytes):
  104. """Test unlock method."""
  105. device = create_device_for_command_testing(model)
  106. device._get_adv_value = Mock(return_value=LockStatus.LOCKED)
  107. with (
  108. patch.object(
  109. device, "_send_command", return_value=b"\x01\x00"
  110. ) as mock_send_command,
  111. patch.object(device, "_enable_notifications", return_value=True),
  112. patch.object(device, "_get_basic_info", return_value=b"\x00\x64\x01"),
  113. ):
  114. result = await device.unlock()
  115. assert result is True
  116. mock_send_command.assert_any_call(lock.COMMAND_UNLOCK[model])
  117. @pytest.mark.asyncio
  118. @pytest.mark.parametrize(
  119. "model",
  120. [
  121. SwitchbotModel.LOCK,
  122. SwitchbotModel.LOCK_LITE,
  123. SwitchbotModel.LOCK_PRO,
  124. SwitchbotModel.LOCK_ULTRA,
  125. SwitchbotModel.LOCK_VISION,
  126. SwitchbotModel.LOCK_VISION_PRO,
  127. SwitchbotModel.LOCK_PRO_WIFI,
  128. ],
  129. )
  130. async def test_unlock_without_unlatch(model: str):
  131. """Test unlock_without_unlatch method."""
  132. device = create_device_for_command_testing(model)
  133. device._get_adv_value = Mock(return_value=LockStatus.LOCKED)
  134. with (
  135. patch.object(device, "_send_command", return_value=b"\x01\x00"),
  136. patch.object(device, "_enable_notifications", return_value=True),
  137. patch.object(device, "_get_basic_info", return_value=b"\x00\x64\x01"),
  138. ):
  139. result = await device.unlock_without_unlatch()
  140. assert result is True
  141. @pytest.mark.asyncio
  142. @pytest.mark.parametrize(
  143. "model",
  144. [
  145. SwitchbotModel.LOCK,
  146. SwitchbotModel.LOCK_LITE,
  147. SwitchbotModel.LOCK_PRO,
  148. SwitchbotModel.LOCK_ULTRA,
  149. SwitchbotModel.LOCK_VISION,
  150. SwitchbotModel.LOCK_VISION_PRO,
  151. SwitchbotModel.LOCK_PRO_WIFI,
  152. ],
  153. )
  154. async def test_get_basic_info(model: str):
  155. """Test get_basic_info method."""
  156. device = create_device_for_command_testing(model)
  157. lock_data = b"\x00\x80\x00\x00\x00\x00\x00\x00"
  158. basic_data = b"\x00\x64\x01"
  159. with (
  160. patch.object(device, "_get_lock_info", return_value=lock_data),
  161. patch.object(device, "_get_basic_info", return_value=basic_data),
  162. ):
  163. result = await device.get_basic_info()
  164. assert result is not None
  165. assert "battery" in result
  166. assert "firmware" in result
  167. assert "calibration" in result
  168. assert "status" in result
  169. @pytest.mark.asyncio
  170. @pytest.mark.parametrize(
  171. "model",
  172. [
  173. SwitchbotModel.LOCK,
  174. SwitchbotModel.LOCK_LITE,
  175. SwitchbotModel.LOCK_PRO,
  176. SwitchbotModel.LOCK_ULTRA,
  177. SwitchbotModel.LOCK_VISION,
  178. SwitchbotModel.LOCK_VISION_PRO,
  179. SwitchbotModel.LOCK_PRO_WIFI,
  180. ],
  181. )
  182. async def test_get_basic_info_no_lock_data(model: str):
  183. """Test get_basic_info when no lock data is returned."""
  184. device = create_device_for_command_testing(model)
  185. with patch.object(device, "_get_lock_info", return_value=None):
  186. result = await device.get_basic_info()
  187. assert result is None
  188. @pytest.mark.asyncio
  189. @pytest.mark.parametrize(
  190. "model",
  191. [
  192. SwitchbotModel.LOCK,
  193. SwitchbotModel.LOCK_LITE,
  194. SwitchbotModel.LOCK_PRO,
  195. SwitchbotModel.LOCK_ULTRA,
  196. SwitchbotModel.LOCK_VISION,
  197. SwitchbotModel.LOCK_VISION_PRO,
  198. SwitchbotModel.LOCK_PRO_WIFI,
  199. ],
  200. )
  201. async def test_get_basic_info_no_basic_data(model: str):
  202. """Test get_basic_info when no basic data is returned."""
  203. device = create_device_for_command_testing(model)
  204. lock_data = b"\x00\x80\x00\x00\x00\x00\x00\x00"
  205. with (
  206. patch.object(device, "_get_lock_info", return_value=lock_data),
  207. patch.object(device, "_get_basic_info", return_value=None),
  208. ):
  209. result = await device.get_basic_info()
  210. assert result is None
  211. def test_parse_basic_data():
  212. """Test _parse_basic_data method."""
  213. device = create_device_for_command_testing(SwitchbotModel.LOCK)
  214. basic_data = b"\x00\x64\x01"
  215. result = device._parse_basic_data(basic_data)
  216. assert result["battery"] == 100
  217. assert result["firmware"] == 0.1
  218. @pytest.mark.parametrize(
  219. "model",
  220. [
  221. SwitchbotModel.LOCK,
  222. SwitchbotModel.LOCK_LITE,
  223. SwitchbotModel.LOCK_PRO,
  224. SwitchbotModel.LOCK_ULTRA,
  225. SwitchbotModel.LOCK_VISION,
  226. SwitchbotModel.LOCK_VISION_PRO,
  227. SwitchbotModel.LOCK_PRO_WIFI,
  228. ],
  229. )
  230. def test_is_calibrated(model: str):
  231. """Test is_calibrated method."""
  232. device = create_device_for_command_testing(model)
  233. device._get_adv_value = Mock(return_value=True)
  234. assert device.is_calibrated() is True
  235. @pytest.mark.parametrize(
  236. "model",
  237. [
  238. SwitchbotModel.LOCK,
  239. SwitchbotModel.LOCK_LITE,
  240. SwitchbotModel.LOCK_PRO,
  241. SwitchbotModel.LOCK_ULTRA,
  242. SwitchbotModel.LOCK_VISION,
  243. SwitchbotModel.LOCK_VISION_PRO,
  244. SwitchbotModel.LOCK_PRO_WIFI,
  245. ],
  246. )
  247. def test_get_lock_status(model: str):
  248. """Test get_lock_status method."""
  249. device = create_device_for_command_testing(model)
  250. device._get_adv_value = Mock(return_value=LockStatus.LOCKED)
  251. assert device.get_lock_status() == LockStatus.LOCKED
  252. @pytest.mark.parametrize(
  253. "model",
  254. [
  255. SwitchbotModel.LOCK,
  256. SwitchbotModel.LOCK_PRO,
  257. SwitchbotModel.LOCK_ULTRA,
  258. SwitchbotModel.LOCK_VISION_PRO,
  259. SwitchbotModel.LOCK_PRO_WIFI,
  260. ],
  261. )
  262. def test_is_door_open(model: str):
  263. """Test is_door_open method."""
  264. device = create_device_for_command_testing(model)
  265. device._get_adv_value = Mock(return_value=True)
  266. assert device.is_door_open() is True
  267. @pytest.mark.parametrize(
  268. "model",
  269. [
  270. SwitchbotModel.LOCK,
  271. SwitchbotModel.LOCK_PRO,
  272. SwitchbotModel.LOCK_ULTRA,
  273. SwitchbotModel.LOCK_VISION_PRO,
  274. SwitchbotModel.LOCK_PRO_WIFI,
  275. ],
  276. )
  277. def test_is_unclosed_alarm_on(model: str):
  278. """Test is_unclosed_alarm_on method."""
  279. device = create_device_for_command_testing(model)
  280. device._get_adv_value = Mock(return_value=True)
  281. assert device.is_unclosed_alarm_on() is True
  282. @pytest.mark.parametrize(
  283. "model",
  284. [
  285. SwitchbotModel.LOCK,
  286. SwitchbotModel.LOCK_LITE,
  287. SwitchbotModel.LOCK_PRO,
  288. SwitchbotModel.LOCK_ULTRA,
  289. SwitchbotModel.LOCK_VISION,
  290. SwitchbotModel.LOCK_VISION_PRO,
  291. SwitchbotModel.LOCK_PRO_WIFI,
  292. ],
  293. )
  294. def test_is_unlocked_alarm_on(model: str):
  295. """Test is_unlocked_alarm_on method."""
  296. device = create_device_for_command_testing(model)
  297. device._get_adv_value = Mock(return_value=True)
  298. assert device.is_unlocked_alarm_on() is True
  299. @pytest.mark.parametrize(
  300. "model",
  301. [
  302. SwitchbotModel.LOCK,
  303. ],
  304. )
  305. def test_is_auto_lock_paused(model: str):
  306. """Test is_auto_lock_paused method."""
  307. device = create_device_for_command_testing(model)
  308. device._get_adv_value = Mock(return_value=True)
  309. assert device.is_auto_lock_paused() is True
  310. @pytest.mark.parametrize(
  311. "model",
  312. [
  313. SwitchbotModel.LOCK,
  314. SwitchbotModel.LOCK_LITE,
  315. SwitchbotModel.LOCK_PRO,
  316. SwitchbotModel.LOCK_ULTRA,
  317. SwitchbotModel.LOCK_VISION,
  318. SwitchbotModel.LOCK_VISION_PRO,
  319. SwitchbotModel.LOCK_PRO_WIFI,
  320. ],
  321. )
  322. def test_is_night_latch_enabled(model: str):
  323. """Test is_night_latch_enabled method."""
  324. device = create_device_for_command_testing(model)
  325. device._get_adv_value = Mock(return_value=True)
  326. assert device.is_night_latch_enabled() is True
  327. @pytest.mark.asyncio
  328. @pytest.mark.parametrize(
  329. "model",
  330. [
  331. SwitchbotModel.LOCK,
  332. SwitchbotModel.LOCK_LITE,
  333. SwitchbotModel.LOCK_PRO,
  334. SwitchbotModel.LOCK_ULTRA,
  335. SwitchbotModel.LOCK_VISION,
  336. SwitchbotModel.LOCK_VISION_PRO,
  337. SwitchbotModel.LOCK_PRO_WIFI,
  338. ],
  339. )
  340. async def test_get_lock_info(model: str):
  341. """Test _get_lock_info method."""
  342. device = create_device_for_command_testing(model)
  343. expected_data = b"\x01\x00\x80\x00\x00\x00\x00\x00"
  344. with patch.object(device, "_send_command", return_value=expected_data):
  345. result = await device._get_lock_info()
  346. assert result == expected_data
  347. @pytest.mark.asyncio
  348. @pytest.mark.parametrize(
  349. "model",
  350. [
  351. SwitchbotModel.LOCK,
  352. SwitchbotModel.LOCK_LITE,
  353. SwitchbotModel.LOCK_PRO,
  354. SwitchbotModel.LOCK_ULTRA,
  355. SwitchbotModel.LOCK_VISION,
  356. SwitchbotModel.LOCK_VISION_PRO,
  357. SwitchbotModel.LOCK_PRO_WIFI,
  358. ],
  359. )
  360. async def test_get_lock_info_failure(model: str):
  361. """Test _get_lock_info method when command fails."""
  362. device = create_device_for_command_testing(model)
  363. with patch.object(device, "_send_command", return_value=b"\x00\x00"):
  364. result = await device._get_lock_info()
  365. assert result is None
  366. @pytest.mark.asyncio
  367. @pytest.mark.parametrize(
  368. "model",
  369. [
  370. SwitchbotModel.LOCK,
  371. SwitchbotModel.LOCK_LITE,
  372. SwitchbotModel.LOCK_PRO,
  373. SwitchbotModel.LOCK_ULTRA,
  374. SwitchbotModel.LOCK_VISION,
  375. SwitchbotModel.LOCK_VISION_PRO,
  376. SwitchbotModel.LOCK_PRO_WIFI,
  377. ],
  378. )
  379. async def test_enable_notifications(model: str):
  380. """Test _enable_notifications method."""
  381. device = create_device_for_command_testing(model)
  382. with patch.object(device, "_send_command", return_value=b"\x01\x00"):
  383. result = await device._enable_notifications()
  384. assert result is True
  385. @pytest.mark.asyncio
  386. @pytest.mark.parametrize(
  387. "model",
  388. [
  389. SwitchbotModel.LOCK,
  390. SwitchbotModel.LOCK_LITE,
  391. SwitchbotModel.LOCK_PRO,
  392. SwitchbotModel.LOCK_ULTRA,
  393. SwitchbotModel.LOCK_VISION,
  394. SwitchbotModel.LOCK_VISION_PRO,
  395. SwitchbotModel.LOCK_PRO_WIFI,
  396. ],
  397. )
  398. async def test_disable_notifications(model: str):
  399. """Test _disable_notifications method."""
  400. device = create_device_for_command_testing(model)
  401. device._notifications_enabled = True
  402. with patch.object(device, "_send_command", return_value=b"\x01\x00"):
  403. result = await device._disable_notifications()
  404. assert result is True
  405. assert device._notifications_enabled is False
  406. @pytest.mark.asyncio
  407. @pytest.mark.parametrize(
  408. "model",
  409. [
  410. SwitchbotModel.LOCK,
  411. SwitchbotModel.LOCK_LITE,
  412. SwitchbotModel.LOCK_PRO,
  413. SwitchbotModel.LOCK_ULTRA,
  414. SwitchbotModel.LOCK_VISION,
  415. SwitchbotModel.LOCK_VISION_PRO,
  416. SwitchbotModel.LOCK_PRO_WIFI,
  417. ],
  418. )
  419. async def test_disable_notifications_already_disabled(model: str):
  420. """Test _disable_notifications when already disabled."""
  421. device = create_device_for_command_testing(model)
  422. device._notifications_enabled = False
  423. with patch.object(device, "_send_command") as mock_send:
  424. result = await device._disable_notifications()
  425. assert result is True
  426. mock_send.assert_not_called()
  427. @pytest.mark.parametrize(
  428. "model",
  429. [
  430. SwitchbotModel.LOCK,
  431. SwitchbotModel.LOCK_LITE,
  432. SwitchbotModel.LOCK_PRO,
  433. SwitchbotModel.LOCK_ULTRA,
  434. SwitchbotModel.LOCK_VISION,
  435. SwitchbotModel.LOCK_VISION_PRO,
  436. SwitchbotModel.LOCK_PRO_WIFI,
  437. ],
  438. )
  439. def test_notification_handler(model: str):
  440. """Test _notification_handler method."""
  441. device = create_device_for_command_testing(model)
  442. device._notifications_enabled = True
  443. data = bytearray(b"\x0f\x00\x00\x00\x80\x00\x00\x00\x00\x00")
  444. with patch.object(device, "_update_lock_status") as mock_update:
  445. device._notification_handler(0, data)
  446. mock_update.assert_called_once_with(data)
  447. @pytest.mark.parametrize(
  448. "model",
  449. [
  450. SwitchbotModel.LOCK,
  451. SwitchbotModel.LOCK_LITE,
  452. SwitchbotModel.LOCK_PRO,
  453. SwitchbotModel.LOCK_ULTRA,
  454. SwitchbotModel.LOCK_VISION,
  455. SwitchbotModel.LOCK_VISION_PRO,
  456. SwitchbotModel.LOCK_PRO_WIFI,
  457. ],
  458. )
  459. def test_notification_handler_not_enabled(model: str):
  460. """Test _notification_handler when notifications not enabled."""
  461. device = create_device_for_command_testing(model)
  462. device._notifications_enabled = False
  463. data = bytearray(b"\x01\x00\x00\x00\x80\x00\x00\x00\x00\x00")
  464. with (
  465. patch.object(device, "_update_lock_status") as mock_update,
  466. patch.object(
  467. device.__class__.__bases__[0], "_notification_handler"
  468. ) as mock_super,
  469. ):
  470. device._notification_handler(0, data)
  471. mock_update.assert_not_called()
  472. mock_super.assert_called_once()
  473. @pytest.mark.parametrize(
  474. "model",
  475. [
  476. SwitchbotModel.LOCK,
  477. SwitchbotModel.LOCK_LITE,
  478. SwitchbotModel.LOCK_PRO,
  479. SwitchbotModel.LOCK_ULTRA,
  480. SwitchbotModel.LOCK_VISION,
  481. SwitchbotModel.LOCK_VISION_PRO,
  482. SwitchbotModel.LOCK_PRO_WIFI,
  483. ],
  484. )
  485. def test_notification_handler_during_disconnect(
  486. model: str, caplog: pytest.LogCaptureFixture
  487. ) -> None:
  488. """Test _notification_handler during expected disconnect."""
  489. device = create_device_for_command_testing(model)
  490. device._notifications_enabled = True
  491. device._expected_disconnect = True
  492. data = bytearray(b"\x0f\x00\x00\x00\x80\x00\x00\x00\x00\x00")
  493. with (
  494. patch.object(device, "_update_lock_status") as mock_update,
  495. caplog.at_level(logging.DEBUG),
  496. ):
  497. device._notification_handler(0, data)
  498. # Should not update lock status during disconnect
  499. mock_update.assert_not_called()
  500. # Should log debug message
  501. assert "Ignoring lock notification during expected disconnect" in caplog.text
  502. @pytest.mark.parametrize(
  503. "model",
  504. [
  505. SwitchbotModel.LOCK,
  506. SwitchbotModel.LOCK_LITE,
  507. SwitchbotModel.LOCK_PRO,
  508. SwitchbotModel.LOCK_ULTRA,
  509. SwitchbotModel.LOCK_VISION,
  510. SwitchbotModel.LOCK_VISION_PRO,
  511. SwitchbotModel.LOCK_PRO_WIFI,
  512. ],
  513. )
  514. def test_update_lock_status(model: str):
  515. """Test _update_lock_status method."""
  516. device = create_device_for_command_testing(model)
  517. data = bytearray(b"\x0f\x00\x00\x00\x80\x00\x00\x00\x00\x00")
  518. with (
  519. patch.object(device, "_decrypt", return_value=b"\x80\x00\x00\x00\x00\x00"),
  520. patch.object(device, "_update_parsed_data", return_value=True),
  521. patch.object(device, "_reset_disconnect_timer"),
  522. patch.object(device, "_fire_callbacks"),
  523. ):
  524. device._update_lock_status(data)
  525. @pytest.mark.parametrize(
  526. ("model", "data", "expected"),
  527. [
  528. (
  529. SwitchbotModel.LOCK,
  530. b"\x80\x00\x00\x00\x00\x00",
  531. {
  532. "calibration": True,
  533. "status": LockStatus.LOCKED, # (0x80 & 0b01110000) >> 4 = 0 = LOCKED
  534. "door_open": False,
  535. "unclosed_alarm": False,
  536. "unlocked_alarm": False,
  537. },
  538. ),
  539. (
  540. SwitchbotModel.LOCK_LITE,
  541. b"\x80\x00\x00\x00\x00\x00",
  542. {
  543. "calibration": True,
  544. "status": LockStatus.LOCKED, # (0x80 & 0b01110000) >> 4 = 0 = LOCKED
  545. "unlocked_alarm": False,
  546. },
  547. ),
  548. (
  549. SwitchbotModel.LOCK_PRO,
  550. b"\x80\x00\x00\x00\x00\x00",
  551. {
  552. "calibration": True,
  553. "status": LockStatus.LOCKED, # (0x80 & 0b01111000) >> 3 = 0 = LOCKED
  554. "door_open": False,
  555. "unclosed_alarm": False,
  556. "unlocked_alarm": False,
  557. },
  558. ),
  559. (
  560. SwitchbotModel.LOCK_ULTRA,
  561. b"\x88\x10\x00\x00\x00\xc0",
  562. {
  563. "calibration": True,
  564. "status": LockStatus.UNLOCKED, # (0x88 & 0b01111000) >> 3 = 0x08 >> 3 = 1 = UNLOCKED
  565. "door_open": True,
  566. "unclosed_alarm": True,
  567. "unlocked_alarm": True,
  568. },
  569. ),
  570. (
  571. SwitchbotModel.LOCK_VISION,
  572. b"\x80\x00\x00\x00\x00\x00",
  573. {
  574. "calibration": True,
  575. "status": LockStatus.LOCKED, # (0x80 & 0b01110000) >> 4 = 0 = LOCKED
  576. "unlocked_alarm": False,
  577. },
  578. ),
  579. (
  580. SwitchbotModel.LOCK_VISION_PRO,
  581. b"\x80\x00\x00\x00\x00\x00",
  582. {
  583. "calibration": True,
  584. "status": LockStatus.LOCKED, # (0x80 & 0b01111000) >> 3 = 0 = LOCKED
  585. "door_open": False,
  586. "unclosed_alarm": False,
  587. "unlocked_alarm": False,
  588. },
  589. ),
  590. (
  591. SwitchbotModel.LOCK_PRO_WIFI,
  592. b"\x80\x00\x00\x00\x00\x00",
  593. {
  594. "calibration": True,
  595. "status": LockStatus.LOCKED, # (0x80 & 0b01111000) >> 3 = 0 = LOCKED
  596. "door_open": False,
  597. "unclosed_alarm": False,
  598. "unlocked_alarm": False,
  599. },
  600. ),
  601. ],
  602. )
  603. def test_parse_lock_data(model: str, data: bytes, expected: dict):
  604. """Test _parse_lock_data static method."""
  605. result = lock.SwitchbotLock._parse_lock_data(data, model)
  606. assert result == expected
  607. @pytest.mark.parametrize(
  608. ("model", "data", "expected"),
  609. [
  610. # Test LOCK with different status bits and flags
  611. (
  612. SwitchbotModel.LOCK,
  613. b"\x94\x00\x00\x00\x00\x00", # Unlocked status (0x10 >> 4 = 1) with door open
  614. {
  615. "calibration": True,
  616. "status": LockStatus.UNLOCKED,
  617. "door_open": True,
  618. "unclosed_alarm": False,
  619. "unlocked_alarm": False,
  620. },
  621. ),
  622. # Test LOCK_LITE without door_open field
  623. (
  624. SwitchbotModel.LOCK_LITE,
  625. b"\x90\x10\x00\x00\x00\x00", # Unlocked with unlocked alarm
  626. {
  627. "calibration": True,
  628. "status": LockStatus.UNLOCKED,
  629. "unlocked_alarm": True,
  630. },
  631. ),
  632. # Test LOCK_PRO with new bit positions
  633. (
  634. SwitchbotModel.LOCK_PRO,
  635. b"\x90\x10\x00\x00\x00\xc0", # New format: status bits 3-6, door open bit 4 of byte 1
  636. {
  637. "calibration": True,
  638. "status": LockStatus.LOCKING, # (0x90 & 0b01111000) >> 3 = 0x10 >> 3 = 2 (LOCKING)
  639. "door_open": True, # bit 4 of byte 1 (0x10)
  640. "unclosed_alarm": True, # bit 7 of byte 5 (0xc0)
  641. "unlocked_alarm": True, # bit 6 of byte 5 (0xc0)
  642. },
  643. ),
  644. # Test LOCK_ULTRA with same format as PRO
  645. (
  646. SwitchbotModel.LOCK_ULTRA,
  647. b"\x88\x00\x00\x00\x00\x40", # Unlocked with unlocked alarm only
  648. {
  649. "calibration": True,
  650. "status": LockStatus.UNLOCKED, # (0x88 & 0b01111000) >> 3 = 0x08 >> 3 = 1
  651. "door_open": False,
  652. "unclosed_alarm": False,
  653. "unlocked_alarm": True, # bit 6 of byte 5
  654. },
  655. ),
  656. (
  657. SwitchbotModel.LOCK_PRO_WIFI,
  658. b"\x90\x10\x00\x00\x00\xc0", # New format: status bits 3-6, door open bit 4 of byte 1
  659. {
  660. "calibration": True,
  661. "status": LockStatus.LOCKING, # (0x90 & 0b01111000) >> 3 = 0x10 >> 3 = 2 (LOCKING)
  662. "door_open": True, # bit 4 of byte 1 (0x10)
  663. "unclosed_alarm": True, # bit 7 of byte 5 (0xc0)
  664. "unlocked_alarm": True, # bit 6 of byte 5 (0xc0)
  665. },
  666. ),
  667. ],
  668. )
  669. def test_parse_lock_data_new_formats(model: str, data: bytes, expected: dict):
  670. """Test _parse_lock_data with new format changes."""
  671. result = lock.SwitchbotLock._parse_lock_data(data, model)
  672. assert result == expected
  673. @pytest.mark.asyncio
  674. @pytest.mark.parametrize(
  675. "model",
  676. [
  677. SwitchbotModel.LOCK,
  678. SwitchbotModel.LOCK_LITE,
  679. SwitchbotModel.LOCK_PRO,
  680. SwitchbotModel.LOCK_ULTRA,
  681. SwitchbotModel.LOCK_VISION,
  682. SwitchbotModel.LOCK_VISION_PRO,
  683. SwitchbotModel.LOCK_PRO_WIFI,
  684. ],
  685. )
  686. async def test_lock_with_update(model: str):
  687. """Test lock method with status update."""
  688. device = create_device_for_command_testing(model)
  689. device._get_adv_value = Mock(side_effect=[None, LockStatus.UNLOCKED])
  690. with (
  691. patch.object(device, "update", new_callable=AsyncMock),
  692. patch.object(device, "_send_command", return_value=b"\x01\x00"),
  693. patch.object(device, "_enable_notifications", return_value=True),
  694. patch.object(device, "_get_basic_info", return_value=b"\x00\x64\x01"),
  695. ):
  696. result = await device.lock()
  697. assert result is True
  698. @pytest.mark.asyncio
  699. @pytest.mark.parametrize(
  700. ("model", "status"),
  701. [
  702. (SwitchbotModel.LOCK, LockStatus.LOCKED),
  703. (SwitchbotModel.LOCK_LITE, LockStatus.LOCKING),
  704. (SwitchbotModel.LOCK_PRO, LockStatus.LOCKED),
  705. (SwitchbotModel.LOCK_ULTRA, LockStatus.LOCKING),
  706. (SwitchbotModel.LOCK_VISION, LockStatus.LOCKED),
  707. (SwitchbotModel.LOCK_VISION_PRO, LockStatus.LOCKED),
  708. (SwitchbotModel.LOCK_PRO_WIFI, LockStatus.LOCKED),
  709. ],
  710. )
  711. async def test_lock_already_locked(model: str, status: LockStatus):
  712. """Test lock method when already locked."""
  713. device = create_device_for_command_testing(model)
  714. device._get_adv_value = Mock(return_value=status)
  715. with patch.object(device, "_send_command") as mock_send:
  716. result = await device.lock()
  717. assert result is True
  718. mock_send.assert_not_called()
  719. @pytest.mark.asyncio
  720. @pytest.mark.parametrize(
  721. "model",
  722. [
  723. SwitchbotModel.LOCK,
  724. SwitchbotModel.LOCK_LITE,
  725. SwitchbotModel.LOCK_PRO,
  726. SwitchbotModel.LOCK_ULTRA,
  727. SwitchbotModel.LOCK_VISION,
  728. SwitchbotModel.LOCK_VISION_PRO,
  729. SwitchbotModel.LOCK_PRO_WIFI,
  730. ],
  731. )
  732. async def test_lock_with_invalid_basic_data(model: str):
  733. """Test lock method with invalid basic data."""
  734. device = create_device_for_command_testing(model)
  735. device._get_adv_value = Mock(return_value=LockStatus.UNLOCKED)
  736. with (
  737. patch.object(device, "_send_command", return_value=b"\x01\x00"),
  738. patch.object(device, "_enable_notifications", return_value=True),
  739. patch.object(device, "_get_basic_info", return_value=b"\x00"),
  740. ):
  741. result = await device.lock()
  742. assert result is True