test_state_dbus.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. # systemctl-mqtt - MQTT client triggering & reporting shutdown on systemd-based systems
  2. #
  3. # Copyright (C) 2020 Fabian Peter Hammerle <fabian@hammerle.me>
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. import datetime
  18. import json
  19. import logging
  20. import re
  21. import typing
  22. import unittest.mock
  23. import jeepney.wrappers
  24. import pytest
  25. import systemctl_mqtt
  26. # pylint: disable=protected-access
  27. def test_shutdown_lock():
  28. lock_fd = unittest.mock.MagicMock(spec=jeepney.fds.FileDescriptor)
  29. with unittest.mock.patch(
  30. "systemctl_mqtt._dbus.get_login_manager_proxy"
  31. ) as get_login_manager_mock:
  32. state = systemctl_mqtt._State(
  33. mqtt_topic_prefix="any",
  34. homeassistant_discovery_prefix=None,
  35. homeassistant_discovery_object_id=None,
  36. poweroff_delay=datetime.timedelta(),
  37. )
  38. get_login_manager_mock.return_value.Inhibit.return_value = (lock_fd,)
  39. state.acquire_shutdown_lock()
  40. state._login_manager.Inhibit.assert_called_once_with(
  41. what="shutdown",
  42. who="systemctl-mqtt",
  43. why="Report shutdown via MQTT",
  44. mode="delay",
  45. )
  46. assert state._shutdown_lock == lock_fd
  47. lock_fd.close.assert_not_called()
  48. state.release_shutdown_lock()
  49. lock_fd.close.assert_called_once_with()
  50. @pytest.mark.parametrize("active", [True, False])
  51. def test_preparing_for_shutdown_handler(active: bool) -> None:
  52. with unittest.mock.patch("systemctl_mqtt._dbus.get_login_manager_proxy"):
  53. state = systemctl_mqtt._State(
  54. mqtt_topic_prefix="any",
  55. homeassistant_discovery_prefix="pre/fix",
  56. homeassistant_discovery_object_id="obj",
  57. poweroff_delay=datetime.timedelta(),
  58. )
  59. mqtt_client_mock = unittest.mock.MagicMock()
  60. with unittest.mock.patch.object(
  61. state, "_publish_preparing_for_shutdown"
  62. ) as publish_mock, unittest.mock.patch.object(
  63. state, "acquire_shutdown_lock"
  64. ) as acquire_lock_mock, unittest.mock.patch.object(
  65. state, "release_shutdown_lock"
  66. ) as release_lock_mock:
  67. state.preparing_for_shutdown_handler(
  68. active=active, mqtt_client=mqtt_client_mock
  69. )
  70. publish_mock.assert_called_once_with(
  71. mqtt_client=mqtt_client_mock, active=active, block=True
  72. )
  73. if active:
  74. acquire_lock_mock.assert_not_called()
  75. release_lock_mock.assert_called_once_with()
  76. else:
  77. acquire_lock_mock.assert_called_once_with()
  78. release_lock_mock.assert_not_called()
  79. @pytest.mark.parametrize("active", [True, False])
  80. def test_publish_preparing_for_shutdown(active: bool) -> None:
  81. login_manager_mock = unittest.mock.MagicMock()
  82. login_manager_mock.Get.return_value = (("b", active),)[:]
  83. with unittest.mock.patch(
  84. "systemctl_mqtt._dbus.get_login_manager_proxy", return_value=login_manager_mock
  85. ):
  86. state = systemctl_mqtt._State(
  87. mqtt_topic_prefix="any",
  88. homeassistant_discovery_prefix="pre/fix",
  89. homeassistant_discovery_object_id="obj",
  90. poweroff_delay=datetime.timedelta(),
  91. )
  92. assert state._login_manager == login_manager_mock
  93. mqtt_client_mock = unittest.mock.MagicMock()
  94. state.publish_preparing_for_shutdown(mqtt_client=mqtt_client_mock)
  95. login_manager_mock.Get.assert_called_once_with("PreparingForShutdown")
  96. mqtt_client_mock.publish.assert_called_once_with(
  97. topic="any/preparing-for-shutdown",
  98. payload="true" if active else "false",
  99. retain=True,
  100. )
  101. class DBusErrorResponseMock(jeepney.wrappers.DBusErrorResponse):
  102. # pylint: disable=missing-class-docstring,super-init-not-called
  103. def __init__(self, name: str, data: typing.Any):
  104. self.name = name
  105. self.data = data
  106. def test_publish_preparing_for_shutdown_get_fail(caplog):
  107. login_manager_mock = unittest.mock.MagicMock()
  108. login_manager_mock.Get.side_effect = DBusErrorResponseMock("error", ("mocked",))
  109. with unittest.mock.patch(
  110. "systemctl_mqtt._dbus.get_login_manager_proxy", return_value=login_manager_mock
  111. ):
  112. state = systemctl_mqtt._State(
  113. mqtt_topic_prefix="any",
  114. homeassistant_discovery_prefix=None,
  115. homeassistant_discovery_object_id=None,
  116. poweroff_delay=datetime.timedelta(),
  117. )
  118. mqtt_client_mock = unittest.mock.MagicMock()
  119. state.publish_preparing_for_shutdown(mqtt_client=None)
  120. mqtt_client_mock.publish.assert_not_called()
  121. assert len(caplog.records) == 1
  122. assert caplog.records[0].levelno == logging.ERROR
  123. assert (
  124. caplog.records[0].message
  125. == "failed to read logind's PreparingForShutdown property: [error] ('mocked',)"
  126. )
  127. @pytest.mark.parametrize("topic_prefix", ["systemctl/hostname", "hostname/systemctl"])
  128. @pytest.mark.parametrize("discovery_prefix", ["homeassistant", "home/assistant"])
  129. @pytest.mark.parametrize("object_id", ["raspberrypi", "debian21"])
  130. @pytest.mark.parametrize("hostname", ["hostname", "host-name"])
  131. def test_publish_homeassistant_device_config(
  132. topic_prefix, discovery_prefix, object_id, hostname
  133. ):
  134. with unittest.mock.patch("jeepney.io.blocking.open_dbus_connection"):
  135. state = systemctl_mqtt._State(
  136. mqtt_topic_prefix=topic_prefix,
  137. homeassistant_discovery_prefix=discovery_prefix,
  138. homeassistant_discovery_object_id=object_id,
  139. poweroff_delay=datetime.timedelta(),
  140. )
  141. mqtt_client = unittest.mock.MagicMock()
  142. with unittest.mock.patch(
  143. "systemctl_mqtt._utils.get_hostname", return_value=hostname
  144. ):
  145. state.publish_homeassistant_device_config(mqtt_client=mqtt_client)
  146. mqtt_client.publish.assert_called_once()
  147. publish_args, publish_kwargs = mqtt_client.publish.call_args
  148. assert not publish_args
  149. assert not publish_kwargs["retain"]
  150. assert (
  151. publish_kwargs["topic"] == discovery_prefix + "/device/" + object_id + "/config"
  152. )
  153. config = json.loads(publish_kwargs["payload"])
  154. assert re.match(r"\d+\.\d+\.", config["origin"].pop("sw_version"))
  155. assert config == {
  156. "origin": {
  157. "name": "systemctl-mqtt",
  158. "support_url": "https://github.com/fphammerle/systemctl-mqtt",
  159. },
  160. "device": {"identifiers": [hostname], "name": hostname},
  161. "components": {
  162. "logind/preparing-for-shutdown": {
  163. "unique_id": f"systemctl-mqtt-{hostname}-logind-preparing-for-shutdown",
  164. "object_id": f"{hostname}_logind_preparing_for_shutdown",
  165. "name": "preparing for shutdown",
  166. "platform": "binary_sensor",
  167. "state_topic": topic_prefix + "/preparing-for-shutdown",
  168. "payload_on": "true",
  169. "payload_off": "false",
  170. },
  171. "logind/poweroff": {
  172. "unique_id": f"systemctl-mqtt-{hostname}-logind-poweroff",
  173. "object_id": f"{hostname}_logind_poweroff",
  174. "name": "poweroff",
  175. "platform": "button",
  176. "command_topic": f"{topic_prefix}/poweroff",
  177. },
  178. "logind/lock-all-sessions": {
  179. "unique_id": f"systemctl-mqtt-{hostname}-logind-lock-all-sessions",
  180. "object_id": f"{hostname}_logind_lock_all_sessions",
  181. "name": "lock all sessions",
  182. "platform": "button",
  183. "command_topic": f"{topic_prefix}/lock-all-sessions",
  184. },
  185. "logind/suspend": {
  186. "unique_id": f"systemctl-mqtt-{hostname}-logind-suspend",
  187. "object_id": f"{hostname}_logind_suspend",
  188. "name": "suspend",
  189. "platform": "button",
  190. "command_topic": f"{topic_prefix}/suspend",
  191. },
  192. },
  193. }