test_fan.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  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.fan import (
  6. FanMode,
  7. HorizontalOscillationAngle,
  8. NightLightState,
  9. StandingFanMode,
  10. VerticalOscillationAngle,
  11. )
  12. from switchbot.devices import fan
  13. from switchbot.devices.device import SwitchbotOperationError
  14. from switchbot.devices.fan import SwitchbotStandingFan
  15. from .test_adv_parser import generate_ble_device
  16. def create_device_for_command_testing(
  17. init_data: dict | None = None, model: SwitchbotModel = SwitchbotModel.CIRCULATOR_FAN
  18. ):
  19. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  20. fan_device = fan.SwitchbotFan(ble_device, model=model)
  21. fan_device.update_from_advertisement(make_advertisement_data(ble_device, init_data))
  22. fan_device._send_command = AsyncMock()
  23. fan_device._check_command_result = MagicMock()
  24. fan_device.update = AsyncMock()
  25. return fan_device
  26. def make_advertisement_data(ble_device: BLEDevice, init_data: dict | None = None):
  27. """Set advertisement data with defaults."""
  28. if init_data is None:
  29. init_data = {}
  30. return SwitchBotAdvertisement(
  31. address="aa:bb:cc:dd:ee:ff",
  32. data={
  33. "rawAdvData": b"~\x00R",
  34. "data": {
  35. "isOn": True,
  36. "mode": "NORMAL",
  37. "nightLight": 3,
  38. "oscillating": False,
  39. "battery": 60,
  40. "speed": 50,
  41. }
  42. | init_data,
  43. "isEncrypted": False,
  44. "model": ",",
  45. "modelFriendlyName": "Circulator Fan",
  46. "modelName": SwitchbotModel.CIRCULATOR_FAN,
  47. },
  48. device=ble_device,
  49. rssi=-80,
  50. active=True,
  51. )
  52. @pytest.mark.asyncio
  53. @pytest.mark.parametrize(
  54. ("response", "expected"),
  55. [
  56. (b"\x00", None),
  57. (b"\x07", None),
  58. (b"\x01\x02\x03", b"\x01\x02\x03"),
  59. ],
  60. )
  61. async def test__get_basic_info(response, expected):
  62. fan_device = create_device_for_command_testing()
  63. fan_device._send_command = AsyncMock(return_value=response)
  64. result = await fan_device._get_basic_info(cmd="TEST_CMD")
  65. assert result == expected
  66. @pytest.mark.asyncio
  67. @pytest.mark.parametrize(
  68. ("basic_info", "firmware_info"), [(True, False), (False, True), (False, False)]
  69. )
  70. async def test_get_basic_info_returns_none(basic_info, firmware_info):
  71. fan_device = create_device_for_command_testing()
  72. async def mock_get_basic_info(arg):
  73. if arg == fan.COMMAND_GET_BASIC_INFO:
  74. return basic_info
  75. if arg == fan.DEVICE_GET_BASIC_SETTINGS_KEY:
  76. return firmware_info
  77. return None
  78. fan_device._get_basic_info = AsyncMock(side_effect=mock_get_basic_info)
  79. assert await fan_device.get_basic_info() is None
  80. @pytest.mark.asyncio
  81. @pytest.mark.parametrize(
  82. ("basic_info", "firmware_info", "result"),
  83. [
  84. (
  85. bytearray(b"\x01\x02W\x82g\xf5\xde4\x01=dPP\x03\x14P\x00\x00\x00\x00"),
  86. bytearray(b"\x01W\x0b\x17\x01"),
  87. [87, True, False, "normal", 61, 1.1],
  88. ),
  89. (
  90. bytearray(b"\x01\x02U\xc2g\xf5\xde4\x04+dPP\x03\x14P\x00\x00\x00\x00"),
  91. bytearray(b"\x01U\x0b\x17\x01"),
  92. [85, True, True, "baby", 43, 1.1],
  93. ),
  94. ],
  95. )
  96. async def test_get_basic_info(basic_info, firmware_info, result):
  97. fan_device = create_device_for_command_testing()
  98. async def mock_get_basic_info(arg):
  99. if arg == fan.COMMAND_GET_BASIC_INFO:
  100. return basic_info
  101. if arg == fan.DEVICE_GET_BASIC_SETTINGS_KEY:
  102. return firmware_info
  103. return None
  104. fan_device._get_basic_info = AsyncMock(side_effect=mock_get_basic_info)
  105. info = await fan_device.get_basic_info()
  106. assert info["battery"] == result[0]
  107. assert info["isOn"] == result[1]
  108. assert info["oscillating"] == result[2]
  109. assert info["mode"] == result[3]
  110. assert info["speed"] == result[4]
  111. assert info["firmware"] == result[5]
  112. @pytest.mark.asyncio
  113. async def test_set_preset_mode():
  114. fan_device = create_device_for_command_testing({"mode": "baby"})
  115. await fan_device.set_preset_mode("baby")
  116. assert fan_device.get_current_mode() == "baby"
  117. @pytest.mark.asyncio
  118. async def test_set_percentage_with_speed_is_0():
  119. fan_device = create_device_for_command_testing({"speed": 0, "isOn": False})
  120. await fan_device.turn_off()
  121. assert fan_device.get_current_percentage() == 0
  122. assert fan_device.is_on() is False
  123. @pytest.mark.asyncio
  124. async def test_set_percentage():
  125. fan_device = create_device_for_command_testing({"speed": 80})
  126. await fan_device.set_percentage(80)
  127. assert fan_device.get_current_percentage() == 80
  128. @pytest.mark.asyncio
  129. async def test_set_not_oscillation():
  130. fan_device = create_device_for_command_testing({"oscillating": False})
  131. await fan_device.set_oscillation(False)
  132. assert fan_device.get_oscillating_state() is False
  133. @pytest.mark.asyncio
  134. async def test_set_oscillation():
  135. fan_device = create_device_for_command_testing({"oscillating": True})
  136. await fan_device.set_oscillation(True)
  137. assert fan_device.get_oscillating_state() is True
  138. @pytest.mark.asyncio
  139. @pytest.mark.parametrize(
  140. ("oscillating", "expected_cmd"),
  141. [
  142. (True, fan.COMMAND_START_OSCILLATION),
  143. (False, fan.COMMAND_STOP_OSCILLATION),
  144. ],
  145. )
  146. async def test_circulator_fan_set_oscillation_command(oscillating, expected_cmd):
  147. """Circulator Fan keeps the original single-axis (V kept) payload."""
  148. fan_device = create_device_for_command_testing({"oscillating": oscillating})
  149. await fan_device.set_oscillation(oscillating)
  150. fan_device._send_command.assert_called_once()
  151. cmd = fan_device._send_command.call_args[0][0]
  152. assert cmd == expected_cmd
  153. def test_circulator_fan_oscillation_command_constants():
  154. """Lock the bytes for the Circulator Fan oscillation commands."""
  155. # These are master-version bytes preserved for backward compatibility.
  156. assert fan.COMMAND_START_OSCILLATION == "570f41020101ff"
  157. assert fan.COMMAND_STOP_OSCILLATION == "570f41020102ff"
  158. @pytest.mark.asyncio
  159. @pytest.mark.parametrize(
  160. ("oscillating", "expected_cmd"),
  161. [
  162. (True, fan.COMMAND_START_OSCILLATION_ALL_AXES),
  163. (False, fan.COMMAND_STOP_OSCILLATION_ALL_AXES),
  164. ],
  165. )
  166. async def test_standing_fan_set_oscillation_command(oscillating, expected_cmd):
  167. """Standing Fan oscillation toggles both axes at once."""
  168. standing_fan = create_standing_fan_for_testing({"oscillating": oscillating})
  169. await standing_fan.set_oscillation(oscillating)
  170. standing_fan._send_command.assert_called_once()
  171. cmd = standing_fan._send_command.call_args[0][0]
  172. assert cmd == expected_cmd
  173. def test_standing_fan_oscillation_command_constants():
  174. """Lock the bytes for the Standing Fan dual-axis oscillation commands."""
  175. assert fan.COMMAND_START_OSCILLATION_ALL_AXES == "570f4102010101"
  176. assert fan.COMMAND_STOP_OSCILLATION_ALL_AXES == "570f4102010202"
  177. def _fan_with_real_result_check(init_data: dict | None = None):
  178. """
  179. Command-test fixture that uses the real _check_command_result.
  180. Unlike `create_device_for_command_testing`, this keeps the real
  181. `_check_command_result` so setter methods exercise the success-byte
  182. validation path.
  183. """
  184. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  185. fan_device = fan.SwitchbotFan(ble_device, model=SwitchbotModel.CIRCULATOR_FAN)
  186. fan_device.update_from_advertisement(make_advertisement_data(ble_device, init_data))
  187. fan_device._send_command = AsyncMock()
  188. fan_device.update = AsyncMock()
  189. return fan_device
  190. def _standing_fan_with_real_result_check(init_data: dict | None = None):
  191. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  192. standing_fan = SwitchbotStandingFan(ble_device, model=SwitchbotModel.STANDING_FAN)
  193. standing_fan.update_from_advertisement(
  194. make_advertisement_data(ble_device, init_data)
  195. )
  196. standing_fan._send_command = AsyncMock()
  197. standing_fan.update = AsyncMock()
  198. return standing_fan
  199. @pytest.mark.asyncio
  200. @pytest.mark.parametrize(
  201. ("response", "expected"),
  202. [
  203. # Success byte is 1.
  204. (b"\x01", True),
  205. (b"\x01\xff", True),
  206. # Known fan error payloads.
  207. (b"\x00", False),
  208. (b"\x07", False),
  209. ],
  210. )
  211. @pytest.mark.parametrize(
  212. "invoke",
  213. [
  214. lambda d: d.set_preset_mode("baby"),
  215. lambda d: d.set_percentage(80),
  216. lambda d: d.set_oscillation(True),
  217. lambda d: d.set_oscillation(False),
  218. lambda d: d.set_horizontal_oscillation(True),
  219. lambda d: d.set_vertical_oscillation(True),
  220. ],
  221. )
  222. async def test_circulator_fan_setters_validate_success_byte(response, expected, invoke):
  223. """Every Circulator Fan setter returns True only on success-byte 1."""
  224. device = _fan_with_real_result_check()
  225. device._send_command.return_value = response
  226. assert await invoke(device) is expected
  227. @pytest.mark.asyncio
  228. @pytest.mark.parametrize(
  229. ("response", "expected"),
  230. [
  231. (b"\x01", True),
  232. (b"\x01\xff", True),
  233. (b"\x00", False),
  234. (b"\x07", False),
  235. ],
  236. )
  237. @pytest.mark.parametrize(
  238. "invoke",
  239. [
  240. lambda d: d.set_horizontal_oscillation_angle(
  241. HorizontalOscillationAngle.ANGLE_60
  242. ),
  243. lambda d: d.set_vertical_oscillation_angle(VerticalOscillationAngle.ANGLE_90),
  244. lambda d: d.set_night_light(NightLightState.LEVEL_1),
  245. lambda d: d.set_night_light(NightLightState.OFF),
  246. ],
  247. )
  248. async def test_standing_fan_setters_validate_success_byte(response, expected, invoke):
  249. """Every Standing Fan setter returns True only on success-byte 1."""
  250. device = _standing_fan_with_real_result_check()
  251. device._send_command.return_value = response
  252. assert await invoke(device) is expected
  253. @pytest.mark.asyncio
  254. async def test_fan_setter_raises_on_none_response():
  255. """None responses raise SwitchbotOperationError via _check_command_result."""
  256. device = _fan_with_real_result_check()
  257. device._send_command.return_value = None
  258. with pytest.raises(SwitchbotOperationError):
  259. await device.set_oscillation(True)
  260. @pytest.mark.asyncio
  261. async def test_turn_on():
  262. fan_device = create_device_for_command_testing({"isOn": True})
  263. await fan_device.turn_on()
  264. assert fan_device.is_on() is True
  265. @pytest.mark.asyncio
  266. async def test_turn_off():
  267. fan_device = create_device_for_command_testing({"isOn": False})
  268. await fan_device.turn_off()
  269. assert fan_device.is_on() is False
  270. def test_get_modes():
  271. assert FanMode.get_modes() == ["normal", "natural", "sleep", "baby"]
  272. def create_standing_fan_for_testing(init_data: dict | None = None):
  273. """Create a SwitchbotStandingFan instance for command testing."""
  274. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  275. standing_fan = SwitchbotStandingFan(ble_device, model=SwitchbotModel.STANDING_FAN)
  276. standing_fan.update_from_advertisement(
  277. make_advertisement_data(ble_device, init_data)
  278. )
  279. standing_fan._send_command = AsyncMock()
  280. standing_fan._check_command_result = MagicMock()
  281. standing_fan.update = AsyncMock()
  282. return standing_fan
  283. def test_standing_fan_inherits_from_switchbot_fan():
  284. assert issubclass(SwitchbotStandingFan, fan.SwitchbotFan)
  285. def test_standing_fan_instantiation():
  286. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  287. standing_fan = SwitchbotStandingFan(ble_device, model=SwitchbotModel.STANDING_FAN)
  288. assert standing_fan is not None
  289. def test_standing_fan_get_modes():
  290. assert StandingFanMode.get_modes() == [
  291. "normal",
  292. "natural",
  293. "sleep",
  294. "baby",
  295. "custom_natural",
  296. ]
  297. @pytest.mark.asyncio
  298. async def test_standing_fan_turn_on():
  299. standing_fan = create_standing_fan_for_testing({"isOn": True})
  300. await standing_fan.turn_on()
  301. assert standing_fan.is_on() is True
  302. @pytest.mark.asyncio
  303. async def test_standing_fan_turn_off():
  304. standing_fan = create_standing_fan_for_testing({"isOn": False})
  305. await standing_fan.turn_off()
  306. assert standing_fan.is_on() is False
  307. @pytest.mark.asyncio
  308. @pytest.mark.parametrize(
  309. "mode",
  310. ["normal", "natural", "sleep", "baby", "custom_natural"],
  311. )
  312. async def test_standing_fan_set_preset_mode(mode):
  313. standing_fan = create_standing_fan_for_testing({"mode": mode})
  314. await standing_fan.set_preset_mode(mode)
  315. assert standing_fan.get_current_mode() == mode
  316. @pytest.mark.asyncio
  317. @pytest.mark.parametrize(
  318. ("basic_info", "firmware_info", "result"),
  319. [
  320. (
  321. bytearray(b"\x01\x02W\x82g\xf5\xde4\x01=dPP\x03\x14P\x00\x00\x00\x00"),
  322. bytearray(b"\x01W\x0b\x17\x01"),
  323. {
  324. "battery": 87,
  325. "isOn": True,
  326. "oscillating": False,
  327. "oscillating_horizontal": False,
  328. "oscillating_vertical": False,
  329. "mode": "normal",
  330. "speed": 61,
  331. "firmware": 1.1,
  332. },
  333. ),
  334. (
  335. bytearray(b"\x01\x02U\xc2g\xf5\xde4\x04+dPP\x03\x14P\x00\x00\x00\x00"),
  336. bytearray(b"\x01U\x0b\x17\x01"),
  337. {
  338. "battery": 85,
  339. "isOn": True,
  340. "oscillating": True,
  341. "oscillating_horizontal": True,
  342. "oscillating_vertical": False,
  343. "mode": "baby",
  344. "speed": 43,
  345. "firmware": 1.1,
  346. },
  347. ),
  348. (
  349. bytearray(b"\x01\x02U\xe2g\xf5\xde4\x05+dPP\x03\x14P\x00\x00\x00\x00"),
  350. bytearray(b"\x01U\x0b\x17\x01"),
  351. {
  352. "battery": 85,
  353. "isOn": True,
  354. "oscillating": True,
  355. "oscillating_horizontal": True,
  356. "oscillating_vertical": True,
  357. "mode": "custom_natural",
  358. "speed": 43,
  359. "firmware": 1.1,
  360. },
  361. ),
  362. ],
  363. )
  364. async def test_standing_fan_get_basic_info(basic_info, firmware_info, result):
  365. # Preload nightLight via the fixture adv data so get_basic_info can surface it.
  366. standing_fan = create_standing_fan_for_testing({"nightLight": 3})
  367. async def mock_get_basic_info(arg):
  368. if arg == fan.COMMAND_GET_BASIC_INFO:
  369. return basic_info
  370. if arg == fan.DEVICE_GET_BASIC_SETTINGS_KEY:
  371. return firmware_info
  372. return None
  373. standing_fan._get_basic_info = AsyncMock(side_effect=mock_get_basic_info)
  374. info = await standing_fan.get_basic_info()
  375. assert info == result | {"nightLight": 3}
  376. @pytest.mark.asyncio
  377. @pytest.mark.parametrize(
  378. ("basic_info", "firmware_info"),
  379. [(True, False), (False, True), (False, False)],
  380. )
  381. async def test_standing_fan_get_basic_info_returns_none(basic_info, firmware_info):
  382. standing_fan = create_standing_fan_for_testing()
  383. async def mock_get_basic_info(arg):
  384. if arg == fan.COMMAND_GET_BASIC_INFO:
  385. return basic_info
  386. if arg == fan.DEVICE_GET_BASIC_SETTINGS_KEY:
  387. return firmware_info
  388. return None
  389. standing_fan._get_basic_info = AsyncMock(side_effect=mock_get_basic_info)
  390. assert await standing_fan.get_basic_info() is None
  391. @pytest.mark.asyncio
  392. @pytest.mark.parametrize(
  393. "angle",
  394. [
  395. HorizontalOscillationAngle.ANGLE_30,
  396. HorizontalOscillationAngle.ANGLE_60,
  397. HorizontalOscillationAngle.ANGLE_90,
  398. ],
  399. )
  400. async def test_standing_fan_set_horizontal_oscillation_angle(angle):
  401. standing_fan = create_standing_fan_for_testing()
  402. await standing_fan.set_horizontal_oscillation_angle(angle)
  403. standing_fan._send_command.assert_called_once()
  404. cmd = standing_fan._send_command.call_args[0][0]
  405. assert cmd == f"{fan.COMMAND_SET_OSCILLATION_PARAMS}{angle.value:02X}FFFFFF"
  406. @pytest.mark.asyncio
  407. @pytest.mark.parametrize("angle", [30, 60, 90])
  408. async def test_standing_fan_set_horizontal_oscillation_angle_int(angle):
  409. """Raw int inputs are coerced through HorizontalOscillationAngle(angle)."""
  410. standing_fan = create_standing_fan_for_testing()
  411. await standing_fan.set_horizontal_oscillation_angle(angle)
  412. cmd = standing_fan._send_command.call_args[0][0]
  413. assert cmd == f"{fan.COMMAND_SET_OSCILLATION_PARAMS}{angle:02X}FFFFFF"
  414. @pytest.mark.asyncio
  415. @pytest.mark.parametrize("angle", [0, 45, 120, -1])
  416. async def test_standing_fan_set_horizontal_oscillation_angle_invalid(angle):
  417. standing_fan = create_standing_fan_for_testing()
  418. with pytest.raises(ValueError, match="is not a valid"):
  419. await standing_fan.set_horizontal_oscillation_angle(angle)
  420. standing_fan._send_command.assert_not_called()
  421. @pytest.mark.asyncio
  422. @pytest.mark.parametrize(
  423. "angle",
  424. [
  425. VerticalOscillationAngle.ANGLE_30,
  426. VerticalOscillationAngle.ANGLE_60,
  427. # Vertical 90° maps to byte 0x5F (95); byte 0x5A (90) halts the axis.
  428. VerticalOscillationAngle.ANGLE_90,
  429. ],
  430. )
  431. async def test_standing_fan_set_vertical_oscillation_angle(angle):
  432. standing_fan = create_standing_fan_for_testing()
  433. await standing_fan.set_vertical_oscillation_angle(angle)
  434. standing_fan._send_command.assert_called_once()
  435. cmd = standing_fan._send_command.call_args[0][0]
  436. assert cmd == f"{fan.COMMAND_SET_OSCILLATION_PARAMS}FFFF{angle.value:02X}FF"
  437. @pytest.mark.asyncio
  438. @pytest.mark.parametrize("byte_value", [30, 60, 95])
  439. async def test_standing_fan_set_vertical_oscillation_angle_int(byte_value):
  440. """Raw-int callers pass the device byte value (30 / 60 / 95)."""
  441. standing_fan = create_standing_fan_for_testing()
  442. await standing_fan.set_vertical_oscillation_angle(byte_value)
  443. cmd = standing_fan._send_command.call_args[0][0]
  444. assert cmd == f"{fan.COMMAND_SET_OSCILLATION_PARAMS}FFFF{byte_value:02X}FF"
  445. @pytest.mark.asyncio
  446. @pytest.mark.parametrize("angle", [0, 45, 120, -1])
  447. async def test_standing_fan_set_vertical_oscillation_angle_invalid(angle):
  448. standing_fan = create_standing_fan_for_testing()
  449. with pytest.raises(ValueError, match="is not a valid"):
  450. await standing_fan.set_vertical_oscillation_angle(angle)
  451. standing_fan._send_command.assert_not_called()
  452. @pytest.mark.asyncio
  453. @pytest.mark.parametrize(
  454. "state",
  455. [NightLightState.LEVEL_1, NightLightState.LEVEL_2, NightLightState.OFF],
  456. )
  457. async def test_standing_fan_set_night_light(state):
  458. standing_fan = create_standing_fan_for_testing()
  459. await standing_fan.set_night_light(state)
  460. standing_fan._send_command.assert_called_once()
  461. cmd = standing_fan._send_command.call_args[0][0]
  462. assert cmd == f"{fan.COMMAND_SET_NIGHT_LIGHT}{state.value:02X}FFFF"
  463. @pytest.mark.asyncio
  464. @pytest.mark.parametrize("state", [1, 2, 3])
  465. async def test_standing_fan_set_night_light_int(state):
  466. """Raw int inputs are coerced through NightLightState(state)."""
  467. standing_fan = create_standing_fan_for_testing()
  468. await standing_fan.set_night_light(state)
  469. cmd = standing_fan._send_command.call_args[0][0]
  470. assert cmd == f"{fan.COMMAND_SET_NIGHT_LIGHT}{state:02X}FFFF"
  471. @pytest.mark.asyncio
  472. @pytest.mark.parametrize("state", [0, 4, 99, -1])
  473. async def test_standing_fan_set_night_light_invalid(state):
  474. standing_fan = create_standing_fan_for_testing()
  475. with pytest.raises(ValueError, match="is not a valid"):
  476. await standing_fan.set_night_light(state)
  477. standing_fan._send_command.assert_not_called()
  478. def test_standing_fan_get_night_light_state():
  479. standing_fan = create_standing_fan_for_testing({"nightLight": 1})
  480. assert standing_fan.get_night_light_state() == 1
  481. @pytest.mark.asyncio
  482. @pytest.mark.parametrize(
  483. ("oscillating", "expected_cmd"),
  484. [
  485. (True, fan.COMMAND_START_HORIZONTAL_OSCILLATION),
  486. (False, fan.COMMAND_STOP_HORIZONTAL_OSCILLATION),
  487. ],
  488. )
  489. async def test_standing_fan_set_horizontal_oscillation(oscillating, expected_cmd):
  490. standing_fan = create_standing_fan_for_testing({"oscillating": oscillating})
  491. await standing_fan.set_horizontal_oscillation(oscillating)
  492. standing_fan._send_command.assert_called_once()
  493. cmd = standing_fan._send_command.call_args[0][0]
  494. assert cmd == expected_cmd
  495. @pytest.mark.asyncio
  496. @pytest.mark.parametrize(
  497. ("oscillating", "expected_cmd"),
  498. [
  499. (True, fan.COMMAND_START_VERTICAL_OSCILLATION),
  500. (False, fan.COMMAND_STOP_VERTICAL_OSCILLATION),
  501. ],
  502. )
  503. async def test_standing_fan_set_vertical_oscillation(oscillating, expected_cmd):
  504. standing_fan = create_standing_fan_for_testing({"oscillating": oscillating})
  505. await standing_fan.set_vertical_oscillation(oscillating)
  506. standing_fan._send_command.assert_called_once()
  507. cmd = standing_fan._send_command.call_args[0][0]
  508. assert cmd == expected_cmd
  509. def test_standing_fan_get_horizontal_oscillating_state():
  510. standing_fan = create_standing_fan_for_testing({"oscillating_horizontal": True})
  511. assert standing_fan.get_horizontal_oscillating_state() is True
  512. def test_standing_fan_get_vertical_oscillating_state():
  513. standing_fan = create_standing_fan_for_testing({"oscillating_vertical": True})
  514. assert standing_fan.get_vertical_oscillating_state() is True