test_switchbot_curtain_motor_position.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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 _pytest.logging # pylint: disable=import-private-name; typing
  22. import pytest
  23. # pylint: disable=import-private-name; internal
  24. from switchbot_mqtt._actors import _CurtainMotor
  25. # pylint: disable=protected-access
  26. @pytest.mark.asyncio
  27. @pytest.mark.parametrize(
  28. ("topic", "payload", "expected_mac_address", "expected_position_percent"),
  29. [
  30. (
  31. "home/cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent",
  32. b"42",
  33. "aa:bb:cc:dd:ee:ff",
  34. 42,
  35. ),
  36. (
  37. "home/cover/switchbot-curtain/11:22:33:44:55:66/position/set-percent",
  38. b"0",
  39. "11:22:33:44:55:66",
  40. 0,
  41. ),
  42. (
  43. "home/cover/switchbot-curtain/11:22:33:44:55:66/position/set-percent",
  44. b"100",
  45. "11:22:33:44:55:66",
  46. 100,
  47. ),
  48. ],
  49. )
  50. @pytest.mark.parametrize("retry_count", (3, 42))
  51. async def test__mqtt_set_position_callback(
  52. caplog: _pytest.logging.LogCaptureFixture,
  53. topic: str,
  54. payload: bytes,
  55. expected_mac_address: str,
  56. retry_count: int,
  57. expected_position_percent: int,
  58. ) -> None:
  59. message = aiomqtt.Message(
  60. topic=topic, payload=payload, qos=0, retain=False, mid=0, properties=None
  61. )
  62. with unittest.mock.patch(
  63. "switchbot.SwitchbotCurtain"
  64. ) as device_init_mock, caplog.at_level(logging.DEBUG):
  65. await _CurtainMotor._mqtt_set_position_callback(
  66. mqtt_client=unittest.mock.Mock(),
  67. message=message,
  68. retry_count=retry_count,
  69. device_passwords={},
  70. fetch_device_info=False,
  71. mqtt_topic_prefix="home/",
  72. )
  73. device_init_mock.assert_called_once_with(
  74. mac=expected_mac_address,
  75. password=None,
  76. retry_count=retry_count,
  77. reverse_mode=True,
  78. )
  79. device_init_mock().set_position.assert_called_once_with(expected_position_percent)
  80. assert caplog.record_tuples == [
  81. (
  82. "switchbot_mqtt._actors",
  83. logging.DEBUG,
  84. f"received topic=home/cover/switchbot-curtain/{expected_mac_address}"
  85. f"/position/set-percent payload=b'{expected_position_percent}'",
  86. ),
  87. (
  88. "switchbot_mqtt._actors",
  89. logging.INFO,
  90. f"set position of switchbot curtain {expected_mac_address}"
  91. f" to {expected_position_percent}%",
  92. ),
  93. ]
  94. @pytest.mark.asyncio
  95. async def test__mqtt_set_position_callback_ignore_retained(
  96. caplog: _pytest.logging.LogCaptureFixture,
  97. ) -> None:
  98. message = aiomqtt.Message(
  99. topic="homeassistant/cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent",
  100. payload=b"42",
  101. qos=0,
  102. retain=True,
  103. mid=0,
  104. properties=None,
  105. )
  106. with unittest.mock.patch(
  107. "switchbot.SwitchbotCurtain"
  108. ) as device_init_mock, caplog.at_level(logging.INFO):
  109. await _CurtainMotor._mqtt_set_position_callback(
  110. mqtt_client=unittest.mock.Mock(),
  111. message=message,
  112. retry_count=3,
  113. device_passwords={},
  114. fetch_device_info=False,
  115. mqtt_topic_prefix="whatever",
  116. )
  117. device_init_mock.assert_not_called()
  118. assert caplog.record_tuples == [
  119. (
  120. "switchbot_mqtt._actors",
  121. logging.INFO,
  122. "ignoring retained message on topic"
  123. " homeassistant/cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent",
  124. ),
  125. ]
  126. @pytest.mark.asyncio
  127. async def test__mqtt_set_position_callback_unexpected_topic(
  128. caplog: _pytest.logging.LogCaptureFixture,
  129. ) -> None:
  130. message = aiomqtt.Message(
  131. topic="switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set",
  132. payload=b"42",
  133. qos=0,
  134. retain=False,
  135. mid=0,
  136. properties=None,
  137. )
  138. with unittest.mock.patch(
  139. "switchbot.SwitchbotCurtain"
  140. ) as device_init_mock, caplog.at_level(logging.INFO):
  141. await _CurtainMotor._mqtt_set_position_callback(
  142. mqtt_client=unittest.mock.Mock(),
  143. message=message,
  144. retry_count=3,
  145. device_passwords={},
  146. fetch_device_info=False,
  147. mqtt_topic_prefix="",
  148. )
  149. device_init_mock.assert_not_called()
  150. assert caplog.record_tuples == [
  151. (
  152. "switchbot_mqtt._actors.base",
  153. logging.WARN,
  154. "unexpected topic switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set",
  155. ),
  156. ]
  157. @pytest.mark.asyncio
  158. async def test__mqtt_set_position_callback_invalid_mac_address(
  159. caplog: _pytest.logging.LogCaptureFixture,
  160. ) -> None:
  161. message = aiomqtt.Message(
  162. topic="tnatsissaemoh/cover/switchbot-curtain/aa:bb:cc:dd:ee/position/set-percent",
  163. payload=b"42",
  164. qos=0,
  165. retain=False,
  166. mid=0,
  167. properties=None,
  168. )
  169. with unittest.mock.patch(
  170. "switchbot.SwitchbotCurtain"
  171. ) as device_init_mock, caplog.at_level(logging.INFO):
  172. await _CurtainMotor._mqtt_set_position_callback(
  173. mqtt_client=unittest.mock.Mock(),
  174. message=message,
  175. retry_count=3,
  176. device_passwords={},
  177. fetch_device_info=False,
  178. mqtt_topic_prefix="tnatsissaemoh/",
  179. )
  180. device_init_mock.assert_not_called()
  181. assert caplog.record_tuples == [
  182. (
  183. "switchbot_mqtt._actors.base",
  184. logging.WARN,
  185. "invalid mac address aa:bb:cc:dd:ee",
  186. ),
  187. ]
  188. @pytest.mark.asyncio
  189. @pytest.mark.parametrize("payload", [b"-1", b"123"])
  190. async def test__mqtt_set_position_callback_invalid_position(
  191. caplog: _pytest.logging.LogCaptureFixture,
  192. payload: bytes,
  193. ) -> None:
  194. message = aiomqtt.Message(
  195. topic="homeassistant/cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent",
  196. payload=payload,
  197. qos=0,
  198. retain=False,
  199. mid=0,
  200. properties=None,
  201. )
  202. with unittest.mock.patch(
  203. "switchbot.SwitchbotCurtain"
  204. ) as device_init_mock, caplog.at_level(logging.INFO):
  205. await _CurtainMotor._mqtt_set_position_callback(
  206. mqtt_client=unittest.mock.Mock(),
  207. message=message,
  208. retry_count=3,
  209. device_passwords={},
  210. fetch_device_info=False,
  211. mqtt_topic_prefix="homeassistant/",
  212. )
  213. device_init_mock.assert_called_once()
  214. device_init_mock().set_position.assert_not_called()
  215. assert caplog.record_tuples == [
  216. (
  217. "switchbot_mqtt._actors",
  218. logging.WARN,
  219. f"invalid position {payload.decode()}%, ignoring message",
  220. ),
  221. ]
  222. @pytest.mark.asyncio
  223. async def test__mqtt_set_position_callback_command_failed(
  224. caplog: _pytest.logging.LogCaptureFixture,
  225. ) -> None:
  226. message = aiomqtt.Message(
  227. topic="cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent",
  228. payload=b"21",
  229. qos=0,
  230. retain=False,
  231. mid=0,
  232. properties=None,
  233. )
  234. with unittest.mock.patch(
  235. "switchbot.SwitchbotCurtain"
  236. ) as device_init_mock, caplog.at_level(logging.INFO):
  237. device_init_mock().set_position.return_value = False
  238. device_init_mock.reset_mock()
  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. device_init_mock().set_position.assert_called_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. ]