test_switchbot_curtain_motor_position.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. # switchbot-mqtt - MQTT client controlling SwitchBot button & curtain automators,
  2. # compatible with home-assistant.io's MQTT Switch & Cover platform
  3. #
  4. # Copyright (C) 2022 Fabian Peter Hammerle <fabian@hammerle.me>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. import logging
  19. import unittest.mock
  20. import aiomqtt
  21. import bleak
  22. import _pytest.logging # pylint: disable=import-private-name; typing
  23. import pytest
  24. # pylint: disable=import-private-name; internal
  25. from switchbot_mqtt._actors import _CurtainMotor
  26. # pylint: disable=protected-access,too-many-positional-arguments
  27. def _create_mqtt_message(
  28. *, topic: str, payload: bytes, retain: bool = False
  29. ) -> aiomqtt.Message:
  30. return aiomqtt.Message(
  31. topic=topic, payload=payload, qos=0, retain=retain, mid=0, properties=None
  32. )
  33. @pytest.mark.asyncio
  34. @pytest.mark.parametrize(
  35. ("topic", "payload", "expected_mac_address", "expected_position_percent"),
  36. [
  37. (
  38. "home/cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent",
  39. b"42",
  40. "aa:bb:cc:dd:ee:ff",
  41. 42,
  42. ),
  43. (
  44. "home/cover/switchbot-curtain/11:22:33:44:55:66/position/set-percent",
  45. b"0",
  46. "11:22:33:44:55:66",
  47. 0,
  48. ),
  49. (
  50. "home/cover/switchbot-curtain/11:22:33:44:55:66/position/set-percent",
  51. b"100",
  52. "11:22:33:44:55:66",
  53. 100,
  54. ),
  55. ],
  56. )
  57. @pytest.mark.parametrize("retry_count", (3, 42))
  58. async def test__mqtt_set_position_callback(
  59. caplog: _pytest.logging.LogCaptureFixture,
  60. topic: str,
  61. payload: bytes,
  62. expected_mac_address: str,
  63. retry_count: int,
  64. expected_position_percent: int,
  65. ) -> None:
  66. message = _create_mqtt_message(topic=topic, payload=payload)
  67. device = unittest.mock.Mock()
  68. device.address = expected_mac_address
  69. with unittest.mock.patch.object(
  70. bleak.BleakScanner, "find_device_by_address", return_value=device
  71. ), unittest.mock.patch(
  72. "switchbot.SwitchbotCurtain.__init__", return_value=None
  73. ) as device_init_mock, unittest.mock.patch(
  74. "switchbot.SwitchbotCurtain.set_position"
  75. ) as set_position_mock, caplog.at_level(
  76. logging.DEBUG
  77. ):
  78. await _CurtainMotor._mqtt_set_position_callback(
  79. mqtt_client=unittest.mock.Mock(),
  80. message=message,
  81. retry_count=retry_count,
  82. device_passwords={},
  83. fetch_device_info=False,
  84. mqtt_topic_prefix="home/",
  85. )
  86. device_init_mock.assert_called_once_with(
  87. device=device, password=None, retry_count=retry_count, reverse_mode=True
  88. )
  89. set_position_mock.assert_called_once_with(expected_position_percent)
  90. assert caplog.record_tuples == [
  91. (
  92. "switchbot_mqtt._actors",
  93. logging.DEBUG,
  94. f"received topic=home/cover/switchbot-curtain/{expected_mac_address}"
  95. f"/position/set-percent payload=b'{expected_position_percent}'",
  96. ),
  97. (
  98. "switchbot_mqtt._actors",
  99. logging.INFO,
  100. f"set position of switchbot curtain {expected_mac_address}"
  101. f" to {expected_position_percent}%",
  102. ),
  103. ]
  104. @pytest.mark.asyncio
  105. async def test__mqtt_set_position_callback_ignore_retained(
  106. caplog: _pytest.logging.LogCaptureFixture,
  107. ) -> None:
  108. message = _create_mqtt_message(
  109. topic="homeassistant/cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent",
  110. payload=b"42",
  111. retain=True,
  112. )
  113. with unittest.mock.patch(
  114. "switchbot.SwitchbotCurtain"
  115. ) as device_init_mock, caplog.at_level(logging.INFO):
  116. await _CurtainMotor._mqtt_set_position_callback(
  117. mqtt_client=unittest.mock.Mock(),
  118. message=message,
  119. retry_count=3,
  120. device_passwords={},
  121. fetch_device_info=False,
  122. mqtt_topic_prefix="whatever",
  123. )
  124. device_init_mock.assert_not_called()
  125. assert caplog.record_tuples == [
  126. (
  127. "switchbot_mqtt._actors",
  128. logging.INFO,
  129. "ignoring retained message on topic"
  130. " homeassistant/cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent",
  131. ),
  132. ]
  133. @pytest.mark.asyncio
  134. async def test__mqtt_set_position_callback_unexpected_topic(
  135. caplog: _pytest.logging.LogCaptureFixture,
  136. ) -> None:
  137. message = _create_mqtt_message(
  138. topic="switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set", payload=b"42"
  139. )
  140. with unittest.mock.patch(
  141. "switchbot.SwitchbotCurtain"
  142. ) as device_init_mock, caplog.at_level(logging.INFO):
  143. await _CurtainMotor._mqtt_set_position_callback(
  144. mqtt_client=unittest.mock.Mock(),
  145. message=message,
  146. retry_count=3,
  147. device_passwords={},
  148. fetch_device_info=False,
  149. mqtt_topic_prefix="",
  150. )
  151. device_init_mock.assert_not_called()
  152. assert caplog.record_tuples == [
  153. (
  154. "switchbot_mqtt._actors.base",
  155. logging.WARN,
  156. "unexpected topic switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set",
  157. ),
  158. ]
  159. @pytest.mark.asyncio
  160. async def test__mqtt_set_position_callback_invalid_mac_address(
  161. caplog: _pytest.logging.LogCaptureFixture,
  162. ) -> None:
  163. message = _create_mqtt_message(
  164. topic="tnatsissaemoh/cover/switchbot-curtain/aa:bb:cc:dd:ee/position/set-percent",
  165. payload=b"42",
  166. )
  167. with unittest.mock.patch(
  168. "switchbot.SwitchbotCurtain"
  169. ) as device_init_mock, caplog.at_level(logging.INFO):
  170. await _CurtainMotor._mqtt_set_position_callback(
  171. mqtt_client=unittest.mock.Mock(),
  172. message=message,
  173. retry_count=3,
  174. device_passwords={},
  175. fetch_device_info=False,
  176. mqtt_topic_prefix="tnatsissaemoh/",
  177. )
  178. device_init_mock.assert_not_called()
  179. assert caplog.record_tuples == [
  180. (
  181. "switchbot_mqtt._actors.base",
  182. logging.WARN,
  183. "invalid mac address aa:bb:cc:dd:ee",
  184. ),
  185. ]
  186. @pytest.mark.asyncio
  187. @pytest.mark.parametrize("payload", [b"-1", b"123"])
  188. async def test__mqtt_set_position_callback_invalid_position(
  189. caplog: _pytest.logging.LogCaptureFixture,
  190. payload: bytes,
  191. ) -> None:
  192. message = _create_mqtt_message(
  193. topic="homeassistant/cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent",
  194. payload=payload,
  195. )
  196. with unittest.mock.patch.object(
  197. bleak.BleakScanner, "find_device_by_address"
  198. ), unittest.mock.patch(
  199. "switchbot.SwitchbotCurtain"
  200. ) as device_init_mock, caplog.at_level(
  201. logging.INFO
  202. ):
  203. await _CurtainMotor._mqtt_set_position_callback(
  204. mqtt_client=unittest.mock.Mock(),
  205. message=message,
  206. retry_count=3,
  207. device_passwords={},
  208. fetch_device_info=False,
  209. mqtt_topic_prefix="homeassistant/",
  210. )
  211. device_init_mock.assert_called_once()
  212. device_init_mock().set_position.assert_not_called()
  213. assert caplog.record_tuples == [
  214. (
  215. "switchbot_mqtt._actors",
  216. logging.WARN,
  217. f"invalid position {payload.decode()}%, ignoring message",
  218. ),
  219. ]
  220. @pytest.mark.asyncio
  221. async def test__mqtt_set_position_callback_command_failed(
  222. caplog: _pytest.logging.LogCaptureFixture,
  223. ) -> None:
  224. message = _create_mqtt_message(
  225. topic="cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent",
  226. payload=b"21",
  227. )
  228. device = unittest.mock.Mock()
  229. device.address = "aa:bb:cc:dd:ee:ff"
  230. with unittest.mock.patch.object(
  231. bleak.BleakScanner, "find_device_by_address", return_value=device
  232. ), unittest.mock.patch(
  233. "switchbot.SwitchbotCurtain.__init__", return_value=None
  234. ) as device_init_mock, unittest.mock.patch(
  235. "switchbot.SwitchbotCurtain.set_position", return_value=False
  236. ) as set_position_mock, caplog.at_level(
  237. logging.INFO
  238. ):
  239. await _CurtainMotor._mqtt_set_position_callback(
  240. mqtt_client=unittest.mock.Mock(),
  241. message=message,
  242. retry_count=3,
  243. device_passwords={},
  244. fetch_device_info=False,
  245. mqtt_topic_prefix="",
  246. )
  247. device_init_mock.assert_called_once()
  248. set_position_mock.assert_awaited_with(21)
  249. assert caplog.record_tuples == [
  250. (
  251. "switchbot_mqtt._actors",
  252. logging.ERROR,
  253. "failed to set position of switchbot curtain aa:bb:cc:dd:ee:ff",
  254. ),
  255. ]