test_state_dbus.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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.login_manager.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. monitored_system_unit_names=[],
  38. controlled_system_unit_names=[],
  39. )
  40. get_login_manager_mock.return_value.Inhibit.return_value = (lock_fd,)
  41. state.acquire_shutdown_lock()
  42. state._login_manager.Inhibit.assert_called_once_with(
  43. what="shutdown",
  44. who="systemctl-mqtt",
  45. why="Report shutdown via MQTT",
  46. mode="delay",
  47. )
  48. assert state._shutdown_lock == lock_fd
  49. lock_fd.close.assert_not_called()
  50. state.release_shutdown_lock()
  51. lock_fd.close.assert_called_once_with()
  52. @pytest.mark.asyncio
  53. @pytest.mark.parametrize("active", [True, False])
  54. async def test_preparing_for_shutdown_handler(active: bool) -> None:
  55. with unittest.mock.patch(
  56. "systemctl_mqtt._dbus.login_manager.get_login_manager_proxy"
  57. ):
  58. state = systemctl_mqtt._State(
  59. mqtt_topic_prefix="any",
  60. homeassistant_discovery_prefix="pre/fix",
  61. homeassistant_discovery_object_id="obj",
  62. poweroff_delay=datetime.timedelta(),
  63. monitored_system_unit_names=[],
  64. controlled_system_unit_names=[],
  65. )
  66. mqtt_client_mock = unittest.mock.MagicMock()
  67. with unittest.mock.patch.object(
  68. state, "_publish_preparing_for_shutdown"
  69. ) as publish_mock, unittest.mock.patch.object(
  70. state, "acquire_shutdown_lock"
  71. ) as acquire_lock_mock, unittest.mock.patch.object(
  72. state, "release_shutdown_lock"
  73. ) as release_lock_mock:
  74. await state.preparing_for_shutdown_handler(
  75. active=active, mqtt_client=mqtt_client_mock
  76. )
  77. publish_mock.assert_awaited_once_with(mqtt_client=mqtt_client_mock, active=active)
  78. if active:
  79. acquire_lock_mock.assert_not_called()
  80. release_lock_mock.assert_called_once_with()
  81. else:
  82. acquire_lock_mock.assert_called_once_with()
  83. release_lock_mock.assert_not_called()
  84. @pytest.mark.asyncio
  85. @pytest.mark.parametrize("active", [True, False])
  86. async def test_publish_preparing_for_shutdown(active: bool) -> None:
  87. login_manager_mock = unittest.mock.MagicMock()
  88. login_manager_mock.Get.return_value = (("b", active),)[:]
  89. with unittest.mock.patch(
  90. "systemctl_mqtt._dbus.login_manager.get_login_manager_proxy",
  91. return_value=login_manager_mock,
  92. ):
  93. state = systemctl_mqtt._State(
  94. mqtt_topic_prefix="any",
  95. homeassistant_discovery_prefix="pre/fix",
  96. homeassistant_discovery_object_id="obj",
  97. poweroff_delay=datetime.timedelta(),
  98. monitored_system_unit_names=[],
  99. controlled_system_unit_names=[],
  100. )
  101. assert state._login_manager == login_manager_mock
  102. mqtt_client_mock = unittest.mock.AsyncMock()
  103. await state.publish_preparing_for_shutdown(mqtt_client=mqtt_client_mock)
  104. login_manager_mock.Get.assert_called_once_with("PreparingForShutdown")
  105. mqtt_client_mock.publish.assert_awaited_once_with(
  106. topic="any/preparing-for-shutdown",
  107. payload="true" if active else "false",
  108. retain=False,
  109. )
  110. class DBusErrorResponseMock(jeepney.wrappers.DBusErrorResponse):
  111. # pylint: disable=missing-class-docstring,super-init-not-called
  112. def __init__(self, name: str, data: typing.Any):
  113. self.name = name
  114. self.data = data
  115. @pytest.mark.asyncio
  116. async def test_publish_preparing_for_shutdown_get_fail(caplog):
  117. login_manager_mock = unittest.mock.MagicMock()
  118. login_manager_mock.Get.side_effect = DBusErrorResponseMock("error", ("mocked",))
  119. with unittest.mock.patch(
  120. "systemctl_mqtt._dbus.login_manager.get_login_manager_proxy",
  121. return_value=login_manager_mock,
  122. ):
  123. state = systemctl_mqtt._State(
  124. mqtt_topic_prefix="any",
  125. homeassistant_discovery_prefix=None,
  126. homeassistant_discovery_object_id=None,
  127. poweroff_delay=datetime.timedelta(),
  128. monitored_system_unit_names=[],
  129. controlled_system_unit_names=[],
  130. )
  131. mqtt_client_mock = unittest.mock.MagicMock()
  132. await state.publish_preparing_for_shutdown(mqtt_client=None)
  133. mqtt_client_mock.publish.assert_not_called()
  134. assert len(caplog.records) == 1
  135. assert caplog.records[0].levelno == logging.ERROR
  136. assert (
  137. caplog.records[0].message
  138. == "failed to read logind's PreparingForShutdown property: [error] ('mocked',)"
  139. )
  140. @pytest.mark.asyncio
  141. @pytest.mark.parametrize("topic_prefix", ["systemctl/hostname", "hostname/systemctl"])
  142. @pytest.mark.parametrize("discovery_prefix", ["homeassistant", "home/assistant"])
  143. @pytest.mark.parametrize("object_id", ["raspberrypi", "debian21"])
  144. @pytest.mark.parametrize("hostname", ["hostname", "host-name"])
  145. @pytest.mark.parametrize(
  146. ("monitored_system_unit_names", "controlled_system_unit_names"),
  147. [
  148. ([], []),
  149. (
  150. ["foo.service", "bar.service"],
  151. ["foo-control.service", "bar-control.service"],
  152. ),
  153. ],
  154. )
  155. async def test_publish_homeassistant_device_config(
  156. # pylint: disable=too-many-arguments,too-many-positional-arguments
  157. topic_prefix: str,
  158. discovery_prefix: str,
  159. object_id: str,
  160. hostname: str,
  161. monitored_system_unit_names: typing.List[str],
  162. controlled_system_unit_names: typing.List[str],
  163. ) -> None:
  164. with unittest.mock.patch("jeepney.io.blocking.open_dbus_connection"):
  165. state = systemctl_mqtt._State(
  166. mqtt_topic_prefix=topic_prefix,
  167. homeassistant_discovery_prefix=discovery_prefix,
  168. homeassistant_discovery_object_id=object_id,
  169. poweroff_delay=datetime.timedelta(),
  170. monitored_system_unit_names=monitored_system_unit_names,
  171. controlled_system_unit_names=controlled_system_unit_names,
  172. )
  173. assert state.monitored_system_unit_names == monitored_system_unit_names
  174. assert state.controlled_system_unit_names == controlled_system_unit_names
  175. mqtt_client = unittest.mock.AsyncMock()
  176. with unittest.mock.patch(
  177. "systemctl_mqtt._utils.get_hostname", return_value=hostname
  178. ):
  179. await state.publish_homeassistant_device_config(mqtt_client=mqtt_client)
  180. mqtt_client.publish.assert_called_once()
  181. publish_args, publish_kwargs = mqtt_client.publish.call_args
  182. assert not publish_args
  183. assert not publish_kwargs["retain"]
  184. assert (
  185. publish_kwargs["topic"] == discovery_prefix + "/device/" + object_id + "/config"
  186. )
  187. config = json.loads(publish_kwargs["payload"])
  188. assert re.match(r"\d+\.\d+\.", config["origin"].pop("sw_version"))
  189. assert config == {
  190. "origin": {
  191. "name": "systemctl-mqtt",
  192. "support_url": "https://github.com/fphammerle/systemctl-mqtt",
  193. },
  194. "device": {"identifiers": [hostname], "name": hostname},
  195. "availability": {"topic": topic_prefix + "/status"},
  196. "components": {
  197. "logind/preparing-for-shutdown": {
  198. "unique_id": f"systemctl-mqtt-{hostname}-logind-preparing-for-shutdown",
  199. "object_id": f"{hostname}_logind_preparing_for_shutdown",
  200. "name": "preparing for shutdown",
  201. "platform": "binary_sensor",
  202. "state_topic": topic_prefix + "/preparing-for-shutdown",
  203. "payload_on": "true",
  204. "payload_off": "false",
  205. },
  206. "logind/poweroff": {
  207. "unique_id": f"systemctl-mqtt-{hostname}-logind-poweroff",
  208. "object_id": f"{hostname}_logind_poweroff",
  209. "name": "poweroff",
  210. "platform": "button",
  211. "command_topic": f"{topic_prefix}/poweroff",
  212. },
  213. "logind/lock-all-sessions": {
  214. "unique_id": f"systemctl-mqtt-{hostname}-logind-lock-all-sessions",
  215. "object_id": f"{hostname}_logind_lock_all_sessions",
  216. "name": "lock all sessions",
  217. "platform": "button",
  218. "command_topic": f"{topic_prefix}/lock-all-sessions",
  219. },
  220. "logind/suspend": {
  221. "unique_id": f"systemctl-mqtt-{hostname}-logind-suspend",
  222. "object_id": f"{hostname}_logind_suspend",
  223. "name": "suspend",
  224. "platform": "button",
  225. "command_topic": f"{topic_prefix}/suspend",
  226. },
  227. }
  228. | {
  229. f"unit/system/{n}/active-state": {
  230. "unique_id": f"systemctl-mqtt-{hostname}-unit-system-{n}-active-state",
  231. "object_id": f"{hostname}_unit_system_{n}_active_state",
  232. "name": f"{n} active state",
  233. "platform": "sensor",
  234. "state_topic": f"{topic_prefix}/unit/system/{n}/active-state",
  235. }
  236. for n in monitored_system_unit_names
  237. }
  238. | {
  239. f"unit/system/{n}/restart": {
  240. "unique_id": f"systemctl-mqtt-{hostname}-unit-system-{n}-restart",
  241. "object_id": f"{hostname}_unit_system_{n}_restart",
  242. "name": f"{n} restart",
  243. "platform": "button",
  244. "command_topic": f"{topic_prefix}/unit/system/{n}/restart",
  245. }
  246. for n in controlled_system_unit_names
  247. },
  248. }