test_mqtt.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  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 typing
  20. import unittest.mock
  21. import pytest
  22. from paho.mqtt.client import MQTT_ERR_QUEUE_SIZE, MQTT_ERR_SUCCESS, MQTTMessage, Client
  23. import switchbot_mqtt
  24. import switchbot_mqtt._actors
  25. from switchbot_mqtt._utils import _MQTTTopicLevel, _MQTTTopicPlaceholder
  26. # pylint: disable=protected-access
  27. # pylint: disable=too-many-arguments; these are tests, no API
  28. @pytest.mark.parametrize("mqtt_host", ["mqtt-broker.local"])
  29. @pytest.mark.parametrize("mqtt_port", [1833])
  30. @pytest.mark.parametrize("retry_count", [3, 21])
  31. @pytest.mark.parametrize(
  32. "device_passwords",
  33. [{}, {"11:22:33:44:55:66": "password", "aa:bb:cc:dd:ee:ff": "secret"}],
  34. )
  35. @pytest.mark.parametrize("fetch_device_info", [True, False])
  36. def test__run(
  37. caplog, mqtt_host, mqtt_port, retry_count, device_passwords, fetch_device_info
  38. ):
  39. with unittest.mock.patch(
  40. "paho.mqtt.client.Client"
  41. ) as mqtt_client_mock, caplog.at_level(logging.DEBUG):
  42. switchbot_mqtt._run(
  43. mqtt_host=mqtt_host,
  44. mqtt_port=mqtt_port,
  45. mqtt_username=None,
  46. mqtt_password=None,
  47. retry_count=retry_count,
  48. device_passwords=device_passwords,
  49. fetch_device_info=fetch_device_info,
  50. )
  51. mqtt_client_mock.assert_called_once()
  52. assert not mqtt_client_mock.call_args[0]
  53. assert set(mqtt_client_mock.call_args[1].keys()) == {"userdata"}
  54. userdata = mqtt_client_mock.call_args[1]["userdata"]
  55. assert userdata == switchbot_mqtt._MQTTCallbackUserdata(
  56. retry_count=retry_count,
  57. device_passwords=device_passwords,
  58. fetch_device_info=fetch_device_info,
  59. )
  60. assert not mqtt_client_mock().username_pw_set.called
  61. mqtt_client_mock().connect.assert_called_once_with(host=mqtt_host, port=mqtt_port)
  62. mqtt_client_mock().socket().getpeername.return_value = (mqtt_host, mqtt_port)
  63. with caplog.at_level(logging.DEBUG):
  64. mqtt_client_mock().on_connect(mqtt_client_mock(), userdata, {}, 0)
  65. subscribe_mock = mqtt_client_mock().subscribe
  66. assert subscribe_mock.call_count == (4 if fetch_device_info else 2)
  67. for topic in [
  68. "homeassistant/switch/switchbot/+/set",
  69. "homeassistant/cover/switchbot-curtain/+/set",
  70. ]:
  71. assert unittest.mock.call(topic) in subscribe_mock.call_args_list
  72. for topic in [
  73. "homeassistant/switch/switchbot/+/request-device-info",
  74. "homeassistant/cover/switchbot-curtain/+/request-device-info",
  75. ]:
  76. assert (
  77. unittest.mock.call(topic) in subscribe_mock.call_args_list
  78. ) == fetch_device_info
  79. mqtt_client_mock().loop_forever.assert_called_once_with()
  80. assert caplog.record_tuples[:2] == [
  81. (
  82. "switchbot_mqtt",
  83. logging.INFO,
  84. f"connecting to MQTT broker {mqtt_host}:{mqtt_port}",
  85. ),
  86. (
  87. "switchbot_mqtt",
  88. logging.DEBUG,
  89. f"connected to MQTT broker {mqtt_host}:{mqtt_port}",
  90. ),
  91. ]
  92. assert len(caplog.record_tuples) == (6 if fetch_device_info else 4)
  93. assert (
  94. "switchbot_mqtt._actors._base",
  95. logging.INFO,
  96. "subscribing to MQTT topic 'homeassistant/switch/switchbot/+/set'",
  97. ) in caplog.record_tuples
  98. assert (
  99. "switchbot_mqtt._actors._base",
  100. logging.INFO,
  101. "subscribing to MQTT topic 'homeassistant/cover/switchbot-curtain/+/set'",
  102. ) in caplog.record_tuples
  103. @pytest.mark.parametrize("mqtt_host", ["mqtt-broker.local"])
  104. @pytest.mark.parametrize("mqtt_port", [1833])
  105. @pytest.mark.parametrize("mqtt_username", ["me"])
  106. @pytest.mark.parametrize("mqtt_password", [None, "secret"])
  107. def test__run_authentication(mqtt_host, mqtt_port, mqtt_username, mqtt_password):
  108. with unittest.mock.patch("paho.mqtt.client.Client") as mqtt_client_mock:
  109. switchbot_mqtt._run(
  110. mqtt_host=mqtt_host,
  111. mqtt_port=mqtt_port,
  112. mqtt_username=mqtt_username,
  113. mqtt_password=mqtt_password,
  114. retry_count=7,
  115. device_passwords={},
  116. fetch_device_info=True,
  117. )
  118. mqtt_client_mock.assert_called_once_with(
  119. userdata=switchbot_mqtt._MQTTCallbackUserdata(
  120. retry_count=7, device_passwords={}, fetch_device_info=True
  121. )
  122. )
  123. mqtt_client_mock().username_pw_set.assert_called_once_with(
  124. username=mqtt_username, password=mqtt_password
  125. )
  126. @pytest.mark.parametrize("mqtt_host", ["mqtt-broker.local"])
  127. @pytest.mark.parametrize("mqtt_port", [1833])
  128. @pytest.mark.parametrize("mqtt_password", ["secret"])
  129. def test__run_authentication_missing_username(mqtt_host, mqtt_port, mqtt_password):
  130. with unittest.mock.patch("paho.mqtt.client.Client"):
  131. with pytest.raises(ValueError):
  132. switchbot_mqtt._run(
  133. mqtt_host=mqtt_host,
  134. mqtt_port=mqtt_port,
  135. mqtt_username=None,
  136. mqtt_password=mqtt_password,
  137. retry_count=3,
  138. device_passwords={},
  139. fetch_device_info=True,
  140. )
  141. def _mock_actor_class(
  142. *,
  143. command_topic_levels: typing.List[_MQTTTopicLevel] = NotImplemented,
  144. request_info_levels: typing.List[_MQTTTopicLevel] = NotImplemented,
  145. ) -> typing.Type:
  146. class _ActorMock(switchbot_mqtt._actors._MQTTControlledActor):
  147. MQTT_COMMAND_TOPIC_LEVELS = command_topic_levels
  148. _MQTT_UPDATE_DEVICE_INFO_TOPIC_LEVELS = request_info_levels
  149. def __init__(self, mac_address, retry_count, password):
  150. super().__init__(
  151. mac_address=mac_address, retry_count=retry_count, password=password
  152. )
  153. def execute_command(
  154. self,
  155. mqtt_message_payload: bytes,
  156. mqtt_client: Client,
  157. update_device_info: bool,
  158. ):
  159. pass
  160. def _get_device(self):
  161. return None
  162. return _ActorMock
  163. @pytest.mark.parametrize(
  164. ("topic_levels", "topic", "expected_mac_address"),
  165. [
  166. (
  167. switchbot_mqtt._actors._ButtonAutomator._MQTT_UPDATE_DEVICE_INFO_TOPIC_LEVELS,
  168. b"homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/request-device-info",
  169. "aa:bb:cc:dd:ee:ff",
  170. ),
  171. ],
  172. )
  173. @pytest.mark.parametrize("payload", [b"", b"whatever"])
  174. def test__mqtt_update_device_info_callback(
  175. caplog,
  176. topic_levels: typing.List[_MQTTTopicLevel],
  177. topic: bytes,
  178. expected_mac_address: str,
  179. payload: bytes,
  180. ):
  181. ActorMock = _mock_actor_class(request_info_levels=topic_levels)
  182. message = MQTTMessage(topic=topic)
  183. message.payload = payload
  184. callback_userdata = switchbot_mqtt._MQTTCallbackUserdata(
  185. retry_count=21, # tested in test__mqtt_command_callback
  186. device_passwords={},
  187. fetch_device_info=True,
  188. )
  189. with unittest.mock.patch.object(
  190. ActorMock, "__init__", return_value=None
  191. ) as init_mock, unittest.mock.patch.object(
  192. ActorMock, "_update_and_report_device_info"
  193. ) as update_mock, caplog.at_level(
  194. logging.DEBUG
  195. ):
  196. ActorMock._mqtt_update_device_info_callback(
  197. "client_dummy", callback_userdata, message
  198. )
  199. init_mock.assert_called_once_with(
  200. mac_address=expected_mac_address, retry_count=21, password=None
  201. )
  202. update_mock.assert_called_once_with("client_dummy")
  203. assert caplog.record_tuples == [
  204. (
  205. "switchbot_mqtt._actors._base",
  206. logging.DEBUG,
  207. f"received topic={topic.decode()} payload={payload!r}",
  208. )
  209. ]
  210. def test__mqtt_update_device_info_callback_ignore_retained(caplog):
  211. ActorMock = _mock_actor_class(
  212. request_info_levels=[_MQTTTopicPlaceholder.MAC_ADDRESS, "request"]
  213. )
  214. message = MQTTMessage(topic=b"aa:bb:cc:dd:ee:ff/request")
  215. message.payload = b""
  216. message.retain = True
  217. with unittest.mock.patch.object(
  218. ActorMock, "__init__", return_value=None
  219. ) as init_mock, unittest.mock.patch.object(
  220. ActorMock, "execute_command"
  221. ) as execute_command_mock, caplog.at_level(
  222. logging.DEBUG
  223. ):
  224. ActorMock._mqtt_update_device_info_callback(
  225. "client_dummy",
  226. switchbot_mqtt._MQTTCallbackUserdata(
  227. retry_count=21, device_passwords={}, fetch_device_info=True
  228. ),
  229. message,
  230. )
  231. init_mock.assert_not_called()
  232. execute_command_mock.assert_not_called()
  233. assert caplog.record_tuples == [
  234. (
  235. "switchbot_mqtt._actors._base",
  236. logging.DEBUG,
  237. "received topic=aa:bb:cc:dd:ee:ff/request payload=b''",
  238. ),
  239. ("switchbot_mqtt._actors._base", logging.INFO, "ignoring retained message"),
  240. ]
  241. @pytest.mark.parametrize(
  242. ("command_topic_levels", "topic", "payload", "expected_mac_address"),
  243. [
  244. (
  245. switchbot_mqtt._ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS,
  246. b"homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/set",
  247. b"ON",
  248. "aa:bb:cc:dd:ee:ff",
  249. ),
  250. (
  251. switchbot_mqtt._ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS,
  252. b"homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/set",
  253. b"OFF",
  254. "aa:bb:cc:dd:ee:ff",
  255. ),
  256. (
  257. switchbot_mqtt._ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS,
  258. b"homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/set",
  259. b"on",
  260. "aa:bb:cc:dd:ee:ff",
  261. ),
  262. (
  263. switchbot_mqtt._ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS,
  264. b"homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/set",
  265. b"off",
  266. "aa:bb:cc:dd:ee:ff",
  267. ),
  268. (
  269. switchbot_mqtt._ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS,
  270. b"homeassistant/switch/switchbot/aa:01:23:45:67:89/set",
  271. b"ON",
  272. "aa:01:23:45:67:89",
  273. ),
  274. (
  275. ["switchbot", _MQTTTopicPlaceholder.MAC_ADDRESS],
  276. b"switchbot/aa:01:23:45:67:89",
  277. b"ON",
  278. "aa:01:23:45:67:89",
  279. ),
  280. (
  281. switchbot_mqtt._CurtainMotor.MQTT_COMMAND_TOPIC_LEVELS,
  282. b"homeassistant/cover/switchbot-curtain/aa:01:23:45:67:89/set",
  283. b"OPEN",
  284. "aa:01:23:45:67:89",
  285. ),
  286. ],
  287. )
  288. @pytest.mark.parametrize("retry_count", (3, 42))
  289. @pytest.mark.parametrize("fetch_device_info", [True, False])
  290. def test__mqtt_command_callback(
  291. caplog,
  292. command_topic_levels: typing.List[_MQTTTopicLevel],
  293. topic: bytes,
  294. payload: bytes,
  295. expected_mac_address: str,
  296. retry_count: int,
  297. fetch_device_info: bool,
  298. ):
  299. ActorMock = _mock_actor_class(command_topic_levels=command_topic_levels)
  300. message = MQTTMessage(topic=topic)
  301. message.payload = payload
  302. callback_userdata = switchbot_mqtt._MQTTCallbackUserdata(
  303. retry_count=retry_count,
  304. device_passwords={},
  305. fetch_device_info=fetch_device_info,
  306. )
  307. with unittest.mock.patch.object(
  308. ActorMock, "__init__", return_value=None
  309. ) as init_mock, unittest.mock.patch.object(
  310. ActorMock, "execute_command"
  311. ) as execute_command_mock, caplog.at_level(
  312. logging.DEBUG
  313. ):
  314. ActorMock._mqtt_command_callback("client_dummy", callback_userdata, message)
  315. init_mock.assert_called_once_with(
  316. mac_address=expected_mac_address, retry_count=retry_count, password=None
  317. )
  318. execute_command_mock.assert_called_once_with(
  319. mqtt_client="client_dummy",
  320. mqtt_message_payload=payload,
  321. update_device_info=fetch_device_info,
  322. )
  323. assert caplog.record_tuples == [
  324. (
  325. "switchbot_mqtt._actors._base",
  326. logging.DEBUG,
  327. f"received topic={topic.decode()} payload={payload!r}",
  328. )
  329. ]
  330. @pytest.mark.parametrize(
  331. ("mac_address", "expected_password"),
  332. [
  333. ("11:22:33:44:55:66", None),
  334. ("aa:bb:cc:dd:ee:ff", "secret"),
  335. ("11:22:33:dd:ee:ff", "äöü"),
  336. ],
  337. )
  338. def test__mqtt_command_callback_password(mac_address, expected_password):
  339. ActorMock = _mock_actor_class(
  340. command_topic_levels=["switchbot", _MQTTTopicPlaceholder.MAC_ADDRESS]
  341. )
  342. message = MQTTMessage(topic=b"switchbot/" + mac_address.encode())
  343. message.payload = b"whatever"
  344. callback_userdata = switchbot_mqtt._MQTTCallbackUserdata(
  345. retry_count=3,
  346. device_passwords={
  347. "11:22:33:44:55:77": "test",
  348. "aa:bb:cc:dd:ee:ff": "secret",
  349. "11:22:33:dd:ee:ff": "äöü",
  350. },
  351. fetch_device_info=True,
  352. )
  353. with unittest.mock.patch.object(
  354. ActorMock, "__init__", return_value=None
  355. ) as init_mock, unittest.mock.patch.object(
  356. ActorMock, "execute_command"
  357. ) as execute_command_mock:
  358. ActorMock._mqtt_command_callback("client_dummy", callback_userdata, message)
  359. init_mock.assert_called_once_with(
  360. mac_address=mac_address, retry_count=3, password=expected_password
  361. )
  362. execute_command_mock.assert_called_once_with(
  363. mqtt_client="client_dummy",
  364. mqtt_message_payload=b"whatever",
  365. update_device_info=True,
  366. )
  367. @pytest.mark.parametrize(
  368. ("topic", "payload"),
  369. [
  370. (b"homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff", b"on"),
  371. (b"homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/change", b"ON"),
  372. (b"homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/set/suffix", b"ON"),
  373. ],
  374. )
  375. def test__mqtt_command_callback_unexpected_topic(caplog, topic: bytes, payload: bytes):
  376. ActorMock = _mock_actor_class(
  377. command_topic_levels=switchbot_mqtt._ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS
  378. )
  379. message = MQTTMessage(topic=topic)
  380. message.payload = payload
  381. with unittest.mock.patch.object(
  382. ActorMock, "__init__", return_value=None
  383. ) as init_mock, unittest.mock.patch.object(
  384. ActorMock, "execute_command"
  385. ) as execute_command_mock, caplog.at_level(
  386. logging.DEBUG
  387. ):
  388. ActorMock._mqtt_command_callback(
  389. "client_dummy",
  390. switchbot_mqtt._MQTTCallbackUserdata(
  391. retry_count=3, device_passwords={}, fetch_device_info=True
  392. ),
  393. message,
  394. )
  395. init_mock.assert_not_called()
  396. execute_command_mock.assert_not_called()
  397. assert caplog.record_tuples == [
  398. (
  399. "switchbot_mqtt._actors._base",
  400. logging.DEBUG,
  401. f"received topic={topic.decode()} payload={payload!r}",
  402. ),
  403. (
  404. "switchbot_mqtt._actors._base",
  405. logging.WARNING,
  406. f"unexpected topic {topic.decode()}",
  407. ),
  408. ]
  409. @pytest.mark.parametrize(("mac_address", "payload"), [("aa:01:23:4E:RR:OR", b"ON")])
  410. def test__mqtt_command_callback_invalid_mac_address(
  411. caplog, mac_address: str, payload: bytes
  412. ):
  413. ActorMock = _mock_actor_class(
  414. command_topic_levels=switchbot_mqtt._ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS
  415. )
  416. topic = f"homeassistant/switch/switchbot/{mac_address}/set".encode()
  417. message = MQTTMessage(topic=topic)
  418. message.payload = payload
  419. with unittest.mock.patch.object(
  420. ActorMock, "__init__", return_value=None
  421. ) as init_mock, unittest.mock.patch.object(
  422. ActorMock, "execute_command"
  423. ) as execute_command_mock, caplog.at_level(
  424. logging.DEBUG
  425. ):
  426. ActorMock._mqtt_command_callback(
  427. "client_dummy",
  428. switchbot_mqtt._MQTTCallbackUserdata(
  429. retry_count=3, device_passwords={}, fetch_device_info=True
  430. ),
  431. message,
  432. )
  433. init_mock.assert_not_called()
  434. execute_command_mock.assert_not_called()
  435. assert caplog.record_tuples == [
  436. (
  437. "switchbot_mqtt._actors._base",
  438. logging.DEBUG,
  439. f"received topic={topic.decode()} payload={payload!r}",
  440. ),
  441. (
  442. "switchbot_mqtt._actors._base",
  443. logging.WARNING,
  444. f"invalid mac address {mac_address}",
  445. ),
  446. ]
  447. @pytest.mark.parametrize(
  448. ("topic", "payload"),
  449. [(b"homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/set", b"ON")],
  450. )
  451. def test__mqtt_command_callback_ignore_retained(caplog, topic: bytes, payload: bytes):
  452. ActorMock = _mock_actor_class(
  453. command_topic_levels=switchbot_mqtt._ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS
  454. )
  455. message = MQTTMessage(topic=topic)
  456. message.payload = payload
  457. message.retain = True
  458. with unittest.mock.patch.object(
  459. ActorMock, "__init__", return_value=None
  460. ) as init_mock, unittest.mock.patch.object(
  461. ActorMock, "execute_command"
  462. ) as execute_command_mock, caplog.at_level(
  463. logging.DEBUG
  464. ):
  465. ActorMock._mqtt_command_callback(
  466. "client_dummy",
  467. switchbot_mqtt._MQTTCallbackUserdata(
  468. retry_count=4, device_passwords={}, fetch_device_info=True
  469. ),
  470. message,
  471. )
  472. init_mock.assert_not_called()
  473. execute_command_mock.assert_not_called()
  474. assert caplog.record_tuples == [
  475. (
  476. "switchbot_mqtt._actors._base",
  477. logging.DEBUG,
  478. f"received topic={topic.decode()} payload={payload!r}",
  479. ),
  480. ("switchbot_mqtt._actors._base", logging.INFO, "ignoring retained message"),
  481. ]
  482. @pytest.mark.parametrize(
  483. ("state_topic_levels", "mac_address", "expected_topic"),
  484. # https://www.home-assistant.io/docs/mqtt/discovery/#switches
  485. [
  486. (
  487. switchbot_mqtt._ButtonAutomator.MQTT_STATE_TOPIC_LEVELS,
  488. "aa:bb:cc:dd:ee:ff",
  489. "homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/state",
  490. ),
  491. (
  492. ["switchbot", _MQTTTopicPlaceholder.MAC_ADDRESS, "state"],
  493. "aa:bb:cc:dd:ee:gg",
  494. "switchbot/aa:bb:cc:dd:ee:gg/state",
  495. ),
  496. ],
  497. )
  498. @pytest.mark.parametrize("state", [b"ON", b"CLOSE"])
  499. @pytest.mark.parametrize("return_code", [MQTT_ERR_SUCCESS, MQTT_ERR_QUEUE_SIZE])
  500. def test__report_state(
  501. caplog,
  502. state_topic_levels: typing.List[_MQTTTopicLevel],
  503. mac_address: str,
  504. expected_topic: str,
  505. state: bytes,
  506. return_code: int,
  507. ):
  508. # pylint: disable=too-many-arguments
  509. class _ActorMock(switchbot_mqtt._actors._MQTTControlledActor):
  510. MQTT_STATE_TOPIC_LEVELS = state_topic_levels
  511. def __init__(self, mac_address, retry_count, password):
  512. super().__init__(
  513. mac_address=mac_address, retry_count=retry_count, password=password
  514. )
  515. def execute_command(
  516. self,
  517. mqtt_message_payload: bytes,
  518. mqtt_client: Client,
  519. update_device_info: bool,
  520. ):
  521. pass
  522. def _get_device(self):
  523. return None
  524. mqtt_client_mock = unittest.mock.MagicMock()
  525. mqtt_client_mock.publish.return_value.rc = return_code
  526. with caplog.at_level(logging.DEBUG):
  527. actor = _ActorMock(mac_address=mac_address, retry_count=3, password=None)
  528. actor.report_state(state=state, mqtt_client=mqtt_client_mock)
  529. mqtt_client_mock.publish.assert_called_once_with(
  530. topic=expected_topic, payload=state, retain=True
  531. )
  532. assert caplog.record_tuples[0] == (
  533. "switchbot_mqtt._actors._base",
  534. logging.DEBUG,
  535. f"publishing topic={expected_topic} payload={state!r}",
  536. )
  537. if return_code == MQTT_ERR_SUCCESS:
  538. assert not caplog.records[1:]
  539. else:
  540. assert caplog.record_tuples[1:] == [
  541. (
  542. "switchbot_mqtt._actors._base",
  543. logging.ERROR,
  544. f"Failed to publish MQTT message on topic {expected_topic} (rc={return_code})",
  545. )
  546. ]