test_relay_switch.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. from unittest.mock import AsyncMock, MagicMock, patch
  2. import pytest
  3. from bleak.backends.device import BLEDevice
  4. from switchbot import SwitchBotAdvertisement, SwitchbotEncryptedDevice, SwitchbotModel
  5. from switchbot.devices import relay_switch
  6. from .test_adv_parser import generate_ble_device
  7. common_params = [
  8. (b";\x00\x00\x00", SwitchbotModel.RELAY_SWITCH_1),
  9. (b"<\x00\x00\x00", SwitchbotModel.RELAY_SWITCH_1PM),
  10. (b">\x00\x00\x00", SwitchbotModel.GARAGE_DOOR_OPENER),
  11. ]
  12. @pytest.fixture
  13. def common_parametrize_2pm():
  14. """Provide common test data."""
  15. return {
  16. "rawAdvData": b"\x00\x00\x00\x00\x00\x00",
  17. "model": SwitchbotModel.RELAY_SWITCH_2PM,
  18. }
  19. def create_device_for_command_testing(
  20. rawAdvData: bytes, model: str, init_data: dict | None = None
  21. ):
  22. """Create a device for command testing."""
  23. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  24. device_class = (
  25. relay_switch.SwitchbotRelaySwitch2PM
  26. if model == SwitchbotModel.RELAY_SWITCH_2PM
  27. else relay_switch.SwitchbotRelaySwitch
  28. )
  29. device = device_class(
  30. ble_device, "ff", "ffffffffffffffffffffffffffffffff", model=model
  31. )
  32. device.update_from_advertisement(
  33. make_advertisement_data(ble_device, rawAdvData, model, init_data)
  34. )
  35. device._send_command = AsyncMock()
  36. device._check_command_result = MagicMock()
  37. device.update = AsyncMock()
  38. return device
  39. def make_advertisement_data(
  40. ble_device: BLEDevice, rawAdvData: bytes, model: str, init_data: dict | None = None
  41. ):
  42. """Set advertisement data with defaults."""
  43. if init_data is None:
  44. init_data = {}
  45. if model == SwitchbotModel.RELAY_SWITCH_2PM:
  46. return SwitchBotAdvertisement(
  47. address="aa:bb:cc:dd:ee:ff",
  48. data={
  49. "rawAdvData": rawAdvData,
  50. "data": {
  51. 1: {
  52. "switchMode": True,
  53. "sequence_number": 99,
  54. "isOn": True,
  55. },
  56. 2: {
  57. "switchMode": True,
  58. "sequence_number": 99,
  59. "isOn": False,
  60. },
  61. }
  62. | init_data,
  63. "isEncrypted": False,
  64. },
  65. device=ble_device,
  66. rssi=-80,
  67. active=True,
  68. )
  69. if model == SwitchbotModel.GARAGE_DOOR_OPENER:
  70. return SwitchBotAdvertisement(
  71. address="aa:bb:cc:dd:ee:ff",
  72. data={
  73. "rawAdvData": rawAdvData,
  74. "data": {
  75. "switchMode": True,
  76. "sequence_number": 96,
  77. "isOn": True,
  78. "door_open": False,
  79. }
  80. | init_data,
  81. "isEncrypted": False,
  82. },
  83. device=ble_device,
  84. rssi=-80,
  85. active=True,
  86. )
  87. return SwitchBotAdvertisement(
  88. address="aa:bb:cc:dd:ee:ff",
  89. data={
  90. "rawAdvData": rawAdvData,
  91. "data": {
  92. "switchMode": True,
  93. "sequence_number": 96,
  94. "isOn": True,
  95. }
  96. | init_data,
  97. "isEncrypted": False,
  98. },
  99. device=ble_device,
  100. rssi=-80,
  101. active=True,
  102. )
  103. @pytest.mark.asyncio
  104. @pytest.mark.parametrize(
  105. "init_data",
  106. [
  107. {1: {"isOn": True}, 2: {"isOn": True}},
  108. ],
  109. )
  110. async def test_turn_on_2PM(common_parametrize_2pm, init_data):
  111. """Test turn on command."""
  112. device = create_device_for_command_testing(
  113. common_parametrize_2pm["rawAdvData"], common_parametrize_2pm["model"], init_data
  114. )
  115. await device.turn_on(1)
  116. device._send_command.assert_called_with(
  117. relay_switch.MULTI_CHANNEL_COMMANDS_TURN_ON[common_parametrize_2pm["model"]][1]
  118. )
  119. assert device.is_on(1) is True
  120. await device.turn_on(2)
  121. device._send_command.assert_called_with(
  122. relay_switch.MULTI_CHANNEL_COMMANDS_TURN_ON[common_parametrize_2pm["model"]][2]
  123. )
  124. assert device.is_on(2) is True
  125. @pytest.mark.asyncio
  126. @pytest.mark.parametrize(
  127. "init_data",
  128. [
  129. {1: {"isOn": False}, 2: {"isOn": False}},
  130. ],
  131. )
  132. async def test_turn_off_2PM(common_parametrize_2pm, init_data):
  133. """Test turn off command."""
  134. device = create_device_for_command_testing(
  135. common_parametrize_2pm["rawAdvData"], common_parametrize_2pm["model"], init_data
  136. )
  137. await device.turn_off(1)
  138. device._send_command.assert_called_with(
  139. relay_switch.MULTI_CHANNEL_COMMANDS_TURN_OFF[common_parametrize_2pm["model"]][1]
  140. )
  141. assert device.is_on(1) is False
  142. await device.turn_off(2)
  143. device._send_command.assert_called_with(
  144. relay_switch.MULTI_CHANNEL_COMMANDS_TURN_OFF[common_parametrize_2pm["model"]][2]
  145. )
  146. assert device.is_on(2) is False
  147. @pytest.mark.asyncio
  148. async def test_turn_toggle_2PM(common_parametrize_2pm):
  149. """Test toggle command."""
  150. device = create_device_for_command_testing(
  151. common_parametrize_2pm["rawAdvData"], common_parametrize_2pm["model"]
  152. )
  153. await device.async_toggle(1)
  154. device._send_command.assert_called_with(
  155. relay_switch.MULTI_CHANNEL_COMMANDS_TOGGLE[common_parametrize_2pm["model"]][1]
  156. )
  157. assert device.is_on(1) is True
  158. await device.async_toggle(2)
  159. device._send_command.assert_called_with(
  160. relay_switch.MULTI_CHANNEL_COMMANDS_TOGGLE[common_parametrize_2pm["model"]][2]
  161. )
  162. assert device.is_on(2) is False
  163. @pytest.mark.asyncio
  164. async def test_get_switch_mode_2PM(common_parametrize_2pm):
  165. """Test get switch mode."""
  166. device = create_device_for_command_testing(
  167. common_parametrize_2pm["rawAdvData"], common_parametrize_2pm["model"]
  168. )
  169. assert device.switch_mode(1) is True
  170. assert device.switch_mode(2) is True
  171. @pytest.mark.asyncio
  172. @pytest.mark.parametrize(
  173. ("info_data", "result"),
  174. [
  175. (
  176. {
  177. "basic_info": b"\x01\x98A\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10",
  178. "channel1_info": b"\x01\x00\x00\x00\x00\x00\x00\x02\x99\x00\xe9\x00\x03\x00\x00",
  179. "channel2_info": b"\x01\x00\x055\x00'<\x02\x9f\x00\xe9\x01,\x00F",
  180. },
  181. [False, 0, 0, 0, 0, True, 0.02, 23, 0.3, 7.0],
  182. ),
  183. (
  184. {
  185. "basic_info": b"\x01\x9e\x81\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10",
  186. "channel1_info": b"\x01\x00\x00\x00\x00\x00\x00\x02\x99\x00\xe9\x00\x03\x00\x00",
  187. "channel2_info": b"\x01\x00\x05\xbc\x00'<\x02\xb1\x00\xea\x01-\x00F",
  188. },
  189. [True, 0, 23, 0.1, 0.0, False, 0.02, 0, 0, 0],
  190. ),
  191. ],
  192. )
  193. async def test_get_basic_info_2PM(common_parametrize_2pm, info_data, result):
  194. """Test get_basic_info for 2PM devices."""
  195. device = create_device_for_command_testing(
  196. common_parametrize_2pm["rawAdvData"], common_parametrize_2pm["model"]
  197. )
  198. assert device.channel == 2
  199. device.get_current_time_and_start_time = MagicMock(
  200. return_value=("683074d6", "682fba80")
  201. )
  202. async def mock_get_basic_info(arg):
  203. if arg == relay_switch.COMMAND_GET_BASIC_INFO:
  204. return info_data["basic_info"]
  205. if arg == relay_switch.COMMAND_GET_CHANNEL1_INFO.format("683074d6", "682fba80"):
  206. return info_data["channel1_info"]
  207. if arg == relay_switch.COMMAND_GET_CHANNEL2_INFO.format("683074d6", "682fba80"):
  208. return info_data["channel2_info"]
  209. return None
  210. device._get_basic_info = AsyncMock(side_effect=mock_get_basic_info)
  211. info = await device.get_basic_info()
  212. assert info is not None
  213. assert 1 in info
  214. assert 2 in info
  215. assert info[1]["isOn"] == result[0]
  216. assert info[1]["energy"] == result[1]
  217. assert info[1]["voltage"] == result[2]
  218. assert info[1]["current"] == result[3]
  219. assert info[1]["power"] == result[4]
  220. assert info[2]["isOn"] == result[5]
  221. assert info[2]["energy"] == result[6]
  222. assert info[2]["voltage"] == result[7]
  223. assert info[2]["current"] == result[8]
  224. assert info[2]["power"] == result[9]
  225. @pytest.mark.asyncio
  226. @pytest.mark.parametrize(
  227. "info_data",
  228. [
  229. {
  230. "basic_info": None,
  231. "channel1_info": b"\x01\x00\x00\x00\x00\x00\x00\x02\x99\x00\xe9\x00\x03\x00\x00",
  232. "channel2_info": b"\x01\x00\x055\x00'<\x02\x9f\x00\xe9\x01,\x00F",
  233. },
  234. {
  235. "basic_info": b"\x01\x98A\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10",
  236. "channel1_info": None,
  237. "channel2_info": b"\x01\x00\x055\x00'<\x02\x9f\x00\xe9\x01,\x00F",
  238. },
  239. {
  240. "basic_info": b"\x01\x98A\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10",
  241. "channel1_info": b"\x01\x00\x00\x00\x00\x00\x00\x02\x99\x00\xe9\x00\x03\x00\x00",
  242. "channel2_info": None,
  243. },
  244. ],
  245. )
  246. async def test_basic_info_exceptions_2PM(common_parametrize_2pm, info_data):
  247. """Test get_basic_info exceptions."""
  248. device = create_device_for_command_testing(
  249. common_parametrize_2pm["rawAdvData"], common_parametrize_2pm["model"]
  250. )
  251. device.get_current_time_and_start_time = MagicMock(
  252. return_value=("683074d6", "682fba80")
  253. )
  254. async def mock_get_basic_info(arg):
  255. if arg == relay_switch.COMMAND_GET_BASIC_INFO:
  256. return info_data["basic_info"]
  257. if arg == relay_switch.COMMAND_GET_CHANNEL1_INFO.format("683074d6", "682fba80"):
  258. return info_data["channel1_info"]
  259. if arg == relay_switch.COMMAND_GET_CHANNEL2_INFO.format("683074d6", "682fba80"):
  260. return info_data["channel2_info"]
  261. return None
  262. device._get_basic_info = AsyncMock(side_effect=mock_get_basic_info)
  263. info = await device.get_basic_info()
  264. assert info is None
  265. @pytest.mark.asyncio
  266. async def test_get_parsed_data_2PM(common_parametrize_2pm):
  267. """Test get_parsed_data for 2PM devices."""
  268. device = create_device_for_command_testing(
  269. common_parametrize_2pm["rawAdvData"], common_parametrize_2pm["model"]
  270. )
  271. info = device.get_parsed_data(1)
  272. assert info["isOn"] is True
  273. info = device.get_parsed_data(2)
  274. assert info["isOn"] is False
  275. @pytest.mark.asyncio
  276. @pytest.mark.parametrize(
  277. ("rawAdvData", "model"),
  278. common_params,
  279. )
  280. async def test_turn_on(rawAdvData, model):
  281. """Test turn on command."""
  282. device = create_device_for_command_testing(rawAdvData, model)
  283. await device.turn_on()
  284. device._send_command.assert_awaited_once_with(relay_switch.COMMAND_TURN_ON)
  285. assert device.is_on() is True
  286. @pytest.mark.asyncio
  287. @pytest.mark.parametrize(
  288. ("rawAdvData", "model"),
  289. common_params,
  290. )
  291. async def test_turn_off(rawAdvData, model):
  292. """Test turn off command."""
  293. device = create_device_for_command_testing(rawAdvData, model, {"isOn": False})
  294. await device.turn_off()
  295. device._send_command.assert_awaited_once_with(relay_switch.COMMAND_TURN_OFF)
  296. assert device.is_on() is False
  297. @pytest.mark.asyncio
  298. @pytest.mark.parametrize(
  299. ("rawAdvData", "model"),
  300. common_params,
  301. )
  302. async def test_toggle(rawAdvData, model):
  303. """Test toggle command."""
  304. device = create_device_for_command_testing(rawAdvData, model)
  305. await device.async_toggle()
  306. device._send_command.assert_awaited_once_with(relay_switch.COMMAND_TOGGLE)
  307. assert device.is_on() is True
  308. @pytest.mark.asyncio
  309. @pytest.mark.parametrize(
  310. ("rawAdvData", "model", "info_data"),
  311. [
  312. (
  313. b">\x00\x00\x00",
  314. SwitchbotModel.GARAGE_DOOR_OPENER,
  315. {
  316. "basic_info": b"\x01>\x80\x0c\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x10",
  317. "channel1_info": b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
  318. },
  319. )
  320. ],
  321. )
  322. async def test_get_basic_info_garage_door_opener(rawAdvData, model, info_data):
  323. """Test get_basic_info for garage door opener."""
  324. device = create_device_for_command_testing(rawAdvData, model)
  325. device.get_current_time_and_start_time = MagicMock(
  326. return_value=("683074d6", "682fba80")
  327. )
  328. async def mock_get_basic_info(arg):
  329. if arg == relay_switch.COMMAND_GET_BASIC_INFO:
  330. return info_data["basic_info"]
  331. if arg == relay_switch.COMMAND_GET_CHANNEL1_INFO.format("683074d6", "682fba80"):
  332. return info_data["channel1_info"]
  333. return None
  334. device._get_basic_info = AsyncMock(side_effect=mock_get_basic_info)
  335. info = await device.get_basic_info()
  336. assert info is not None
  337. assert info["isOn"] is True
  338. assert info["door_open"] is True
  339. @pytest.mark.asyncio
  340. @pytest.mark.parametrize(
  341. "model",
  342. [
  343. SwitchbotModel.RELAY_SWITCH_1,
  344. SwitchbotModel.RELAY_SWITCH_1PM,
  345. SwitchbotModel.GARAGE_DOOR_OPENER,
  346. SwitchbotModel.RELAY_SWITCH_2PM,
  347. ],
  348. )
  349. @patch.object(SwitchbotEncryptedDevice, "verify_encryption_key", new_callable=AsyncMock)
  350. async def test_verify_encryption_key(mock_parent_verify, model):
  351. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  352. key_id = "ff"
  353. encryption_key = "ffffffffffffffffffffffffffffffff"
  354. mock_parent_verify.return_value = True
  355. result = await relay_switch.SwitchbotRelaySwitch.verify_encryption_key(
  356. device=ble_device,
  357. key_id=key_id,
  358. encryption_key=encryption_key,
  359. model=model,
  360. )
  361. mock_parent_verify.assert_awaited_once_with(
  362. ble_device,
  363. key_id,
  364. encryption_key,
  365. model,
  366. )
  367. assert result is True