test_switchbot_curtain_motor.py 9.9 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) 2020 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 bluepy.btle
  21. import pytest
  22. import switchbot_mqtt
  23. # pylint: disable=protected-access,
  24. # pylint: disable=too-many-arguments; these are tests, no API
  25. @pytest.mark.parametrize(
  26. "mac_address",
  27. ("aa:bb:cc:dd:ee:ff", "aa:bb:cc:dd:ee:gg"),
  28. )
  29. @pytest.mark.parametrize(
  30. ("position", "expected_payload"), [(0, b"0"), (100, b"100"), (42, b"42")]
  31. )
  32. def test__report_position(
  33. caplog, mac_address: str, position: int, expected_payload: bytes
  34. ):
  35. with unittest.mock.patch(
  36. "switchbot.SwitchbotCurtain.__init__", return_value=None
  37. ) as device_init_mock, caplog.at_level(logging.DEBUG):
  38. actor = switchbot_mqtt._CurtainMotor(
  39. mac_address=mac_address, retry_count=7, password=None
  40. )
  41. device_init_mock.assert_called_once_with(
  42. mac=mac_address,
  43. retry_count=7,
  44. password=None,
  45. # > The position of the curtain is saved in self._pos with 0 = open and 100 = closed.
  46. # > [...] The parameter 'reverse_mode' reverse these values, [...]
  47. # > The parameter is default set to True so that the definition of position
  48. # > is the same as in Home Assistant.
  49. # https://github.com/Danielhiversen/pySwitchbot/blob/0.10.0/switchbot/__init__.py#L150
  50. reverse_mode=True,
  51. )
  52. with unittest.mock.patch.object(
  53. actor, "_mqtt_publish"
  54. ) as publish_mock, unittest.mock.patch(
  55. "switchbot.SwitchbotCurtain.get_position", return_value=position
  56. ):
  57. actor._report_position(mqtt_client="dummy")
  58. publish_mock.assert_called_once_with(
  59. topic_levels=[
  60. "homeassistant",
  61. "cover",
  62. "switchbot-curtain",
  63. switchbot_mqtt._MQTTTopicPlaceholder.MAC_ADDRESS,
  64. "position",
  65. ],
  66. payload=expected_payload,
  67. mqtt_client="dummy",
  68. )
  69. assert not caplog.record_tuples
  70. @pytest.mark.parametrize("position", ("", 'lambda: print("")'))
  71. def test__report_position_invalid(caplog, position):
  72. with unittest.mock.patch(
  73. "switchbot.SwitchbotCurtain.__init__", return_value=None
  74. ), caplog.at_level(logging.DEBUG):
  75. actor = switchbot_mqtt._CurtainMotor(
  76. mac_address="aa:bb:cc:dd:ee:ff", retry_count=3, password=None
  77. )
  78. with unittest.mock.patch.object(
  79. actor, "_mqtt_publish"
  80. ) as publish_mock, unittest.mock.patch(
  81. "switchbot.SwitchbotCurtain.get_position", return_value=position
  82. ), pytest.raises(
  83. ValueError
  84. ):
  85. actor._report_position(mqtt_client="dummy")
  86. publish_mock.assert_not_called()
  87. def test__update_position():
  88. with unittest.mock.patch("switchbot.SwitchbotCurtain.__init__", return_value=None):
  89. actor = switchbot_mqtt._CurtainMotor(
  90. mac_address="dummy", retry_count=21, password=None
  91. )
  92. with unittest.mock.patch(
  93. "switchbot.SwitchbotCurtain.update"
  94. ) as update_mock, unittest.mock.patch.object(
  95. actor, "_report_position"
  96. ) as report_position_mock:
  97. actor._update_position(mqtt_client="client")
  98. update_mock.assert_called_once_with()
  99. report_position_mock.assert_called_once_with(mqtt_client="client")
  100. @pytest.mark.parametrize("mac_address", ["aa:bb:cc:dd:ee:ff", "aa:bb:cc:11:22:33"])
  101. @pytest.mark.parametrize("password", ["pa$$word", None])
  102. @pytest.mark.parametrize("retry_count", (2, 3))
  103. @pytest.mark.parametrize(
  104. ("message_payload", "action_name"),
  105. [
  106. (b"open", "switchbot.SwitchbotCurtain.open"),
  107. (b"OPEN", "switchbot.SwitchbotCurtain.open"),
  108. (b"Open", "switchbot.SwitchbotCurtain.open"),
  109. (b"close", "switchbot.SwitchbotCurtain.close"),
  110. (b"CLOSE", "switchbot.SwitchbotCurtain.close"),
  111. (b"Close", "switchbot.SwitchbotCurtain.close"),
  112. (b"stop", "switchbot.SwitchbotCurtain.stop"),
  113. (b"STOP", "switchbot.SwitchbotCurtain.stop"),
  114. (b"Stop", "switchbot.SwitchbotCurtain.stop"),
  115. ],
  116. )
  117. @pytest.mark.parametrize("report_position_upon_stop", [True, False])
  118. @pytest.mark.parametrize("command_successful", [True, False])
  119. def test_execute_command(
  120. caplog,
  121. mac_address,
  122. password,
  123. retry_count,
  124. message_payload,
  125. action_name,
  126. report_position_upon_stop,
  127. command_successful,
  128. ):
  129. with unittest.mock.patch(
  130. "switchbot.SwitchbotCurtain.__init__", return_value=None
  131. ) as device_init_mock, caplog.at_level(logging.INFO):
  132. actor = switchbot_mqtt._CurtainMotor(
  133. mac_address=mac_address, retry_count=retry_count, password=password
  134. )
  135. with unittest.mock.patch.object(
  136. actor, "report_state"
  137. ) as report_mock, unittest.mock.patch(
  138. action_name, return_value=command_successful
  139. ) as action_mock, unittest.mock.patch.object(
  140. actor, "_update_position"
  141. ) as update_position_mock:
  142. actor.execute_command(
  143. mqtt_client="dummy",
  144. mqtt_message_payload=message_payload,
  145. update_device_info=report_position_upon_stop,
  146. )
  147. device_init_mock.assert_called_once_with(
  148. mac=mac_address, password=password, retry_count=retry_count, reverse_mode=True
  149. )
  150. action_mock.assert_called_once_with()
  151. if command_successful:
  152. assert caplog.record_tuples == [
  153. (
  154. "switchbot_mqtt",
  155. logging.INFO,
  156. "switchbot curtain {} {}".format(
  157. mac_address,
  158. {b"open": "opening", b"close": "closing", b"stop": "stopped"}[
  159. message_payload.lower()
  160. ],
  161. ),
  162. )
  163. ]
  164. report_mock.assert_called_once_with(
  165. mqtt_client="dummy",
  166. # https://www.home-assistant.io/integrations/cover.mqtt/#state_opening
  167. state={b"open": b"opening", b"close": b"closing", b"stop": b""}[
  168. message_payload.lower()
  169. ],
  170. )
  171. else:
  172. assert caplog.record_tuples == [
  173. (
  174. "switchbot_mqtt",
  175. logging.ERROR,
  176. "failed to {} switchbot curtain {}".format(
  177. message_payload.decode().lower(), mac_address
  178. ),
  179. )
  180. ]
  181. report_mock.assert_not_called()
  182. if (
  183. report_position_upon_stop
  184. and action_name == "switchbot.SwitchbotCurtain.stop"
  185. and command_successful
  186. ):
  187. update_position_mock.assert_called_once_with(mqtt_client="dummy")
  188. else:
  189. update_position_mock.assert_not_called()
  190. @pytest.mark.parametrize("mac_address", ["aa:bb:cc:dd:ee:ff"])
  191. @pytest.mark.parametrize("password", ["secret"])
  192. @pytest.mark.parametrize("message_payload", [b"OEFFNEN", b""])
  193. def test_execute_command_invalid_payload(
  194. caplog, mac_address, password, message_payload
  195. ):
  196. with unittest.mock.patch(
  197. "switchbot.SwitchbotCurtain"
  198. ) as device_mock, caplog.at_level(logging.INFO):
  199. actor = switchbot_mqtt._CurtainMotor(
  200. mac_address=mac_address, retry_count=7, password=password
  201. )
  202. with unittest.mock.patch.object(actor, "report_state") as report_mock:
  203. actor.execute_command(
  204. mqtt_client="dummy",
  205. mqtt_message_payload=message_payload,
  206. update_device_info=True,
  207. )
  208. device_mock.assert_called_once_with(
  209. mac=mac_address, password=password, retry_count=7, reverse_mode=True
  210. )
  211. assert not device_mock().mock_calls # no methods called
  212. report_mock.assert_not_called()
  213. assert caplog.record_tuples == [
  214. (
  215. "switchbot_mqtt",
  216. logging.WARNING,
  217. "unexpected payload {!r} (expected 'OPEN', 'CLOSE', or 'STOP')".format(
  218. message_payload
  219. ),
  220. )
  221. ]
  222. @pytest.mark.parametrize("mac_address", ["aa:bb:cc:dd:ee:ff"])
  223. @pytest.mark.parametrize("message_payload", [b"OPEN", b"CLOSE", b"STOP"])
  224. def test_execute_command_bluetooth_error(caplog, mac_address, message_payload):
  225. """
  226. paho.mqtt.python>=1.5.1 no longer implicitly suppresses exceptions in callbacks.
  227. verify pySwitchbot catches exceptions raised in bluetooth stack.
  228. https://github.com/Danielhiversen/pySwitchbot/blob/0.8.0/switchbot/__init__.py#L48
  229. https://github.com/Danielhiversen/pySwitchbot/blob/0.8.0/switchbot/__init__.py#L94
  230. """
  231. with unittest.mock.patch(
  232. "bluepy.btle.Peripheral",
  233. side_effect=bluepy.btle.BTLEDisconnectError(
  234. "Failed to connect to peripheral {}, addr type: random".format(mac_address)
  235. ),
  236. ), caplog.at_level(logging.ERROR):
  237. switchbot_mqtt._CurtainMotor(
  238. mac_address=mac_address, retry_count=10, password="secret"
  239. ).execute_command(
  240. mqtt_client="dummy",
  241. mqtt_message_payload=message_payload,
  242. update_device_info=True,
  243. )
  244. assert caplog.record_tuples == [
  245. (
  246. "switchbot",
  247. logging.ERROR,
  248. "Switchbot communication failed. Stopping trying.",
  249. ),
  250. (
  251. "switchbot_mqtt",
  252. logging.ERROR,
  253. "failed to {} switchbot curtain {}".format(
  254. message_payload.decode().lower(), mac_address
  255. ),
  256. ),
  257. ]