Преглед на файлове

suspend when receiving message on topic `systemctl/[hostname]/suspend`

implements feature request https://github.com/fphammerle/systemctl-mqtt/issues/97
Fabian Peter Hammerle преди 3 месеца
родител
ревизия
9566cf4e69
променени са 9 файла, в които са добавени 70 реда и са изтрити 9 реда
  1. 10 7
      CHANGELOG.md
  2. 7 0
      README.md
  3. 10 0
      systemctl_mqtt/__init__.py
  4. 10 0
      systemctl_mqtt/_dbus.py
  5. 1 0
      tests/dbus/message-generators/test_login_manager.py
  6. 10 0
      tests/test_action.py
  7. 12 0
      tests/test_dbus.py
  8. 3 2
      tests/test_mqtt.py
  9. 7 0
      tests/test_state_dbus.py

+ 10 - 7
CHANGELOG.md

@@ -6,9 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 ### Added
+- suspend when receiving message on topic `systemctl/[hostname]/suspend`
+  (https://github.com/fphammerle/systemctl-mqtt/issues/97)
 - automatic discovery in home assistant:
   - entity `button.[hostname]_logind_lock_all_sessions`
   - entity `button.[hostname]_logind_poweroff`
+  - entity `button.[hostname]_logind_suspend`
 - declare compatibility with `python3.11`, `python3.12` & `python3.13`
 
 ### Changed
@@ -28,7 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
     `binary_sensor.[hostname]_logind_preparing_for_shutdown`
   - disable "retain" flag for discovery messages
     (to avoid reappearing ghost devices)
-- docker image:
+- container image / dockerfile:
   - upgrade alpine base image from 3.13.1 to 3.21.0 including upgrade of python
     from 3.8 to 3.12
   - support build without git history by manually setting build argument
@@ -37,12 +40,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Fixed
 - apparmor profile for architectures other than x86_64/amd64
   (`ImportError: Error loading [...]/_gi.cpython-38-aarch64-linux-gnu.so: Permission denied`)
-- dockerfile: split `pipenv install` into two stages to speed up image builds
-- dockerfile: `chmod` files copied from host to no longer require `o=rX` perms on host
-- dockerfile: add registry to base image specifier for `podman build`
-- dockerfile: add `--force` flag to `rm` invocation to avoid interactive questions while running `podman build`
-- dockerfile: ignore "sanitized-package" added to `Pipfile.lock` by dependabot
-  (fixes `pipenv.vendor.requirementslib.exceptions.RequirementError: Failed parsing requirement from '.'`)
+- container image / dockerfile:
+  - split `pipenv install` into two stages to speed up image builds
+  - `chmod` files copied from host to no longer require `o=rX` perms on host
+  - add registry to base image specifier for `podman build`
+  - add `--force` flag to `rm` invocation to avoid interactive questions while
+    running `podman build`
 
 ### Removed
 - compatibility with `python3.5`, `python3.6`, `python3.7` & `python3.8`

+ 7 - 0
README.md

@@ -45,6 +45,12 @@ Lock screen by sending a MQTT message to topic `systemctl/hostname/lock-all-sess
 $ mosquitto_pub -h MQTT_BROKER -t systemctl/hostname/lock-all-sessions -n
 ```
 
+### Suspend
+
+```
+$ mosquitto_pub -h MQTT_BROKER -t systemctl/hostname/suspend -n
+```
+
 ## Home Assistant 🏡
 
 ### Automatic Discovery
@@ -55,6 +61,7 @@ added automatically:
 - `binary_sensor.[hostname]_logind_preparing_for_shutdown`
 - `button.[hostname]_logind_lock_all_sessions`
 - `button.[hostname]_logind_poweroff`
+- `button.[hostname]_logind_suspend`
 
 ![homeassistant entities_over_auto_discovery](docs/homeassistant/entities-after-auto-discovery.png)
 

+ 10 - 0
systemctl_mqtt/__init__.py

@@ -244,9 +244,19 @@ class _MQTTActionLockAllSessions(_MQTTAction):
         return type(self).__name__
 
 
+class _MQTTActionSuspend(_MQTTAction):
+    def trigger(self, state: _State) -> None:
+        # pylint: disable=protected-access
+        systemctl_mqtt._dbus.suspend()
+
+    def __str__(self) -> str:
+        return type(self).__name__
+
+
 _MQTT_TOPIC_SUFFIX_ACTION_MAPPING = {
     "poweroff": _MQTTActionSchedulePoweroff(),
     "lock-all-sessions": _MQTTActionLockAllSessions(),
+    "suspend": _MQTTActionSuspend(),
 }
 
 

+ 10 - 0
systemctl_mqtt/_dbus.py

@@ -73,6 +73,11 @@ class LoginManager(jeepney.MessageGenerator):
             body=(action, int(time.timestamp() * 1e6)),  # (type, usec)
         )
 
+    def Suspend(self, *, interactive: bool) -> jeepney.low_level.Message:
+        return jeepney.new_method_call(
+            remote_obj=self, method="Suspend", signature="b", body=(interactive,)
+        )
+
     def Inhibit(
         self, *, what: str, who: str, why: str, mode: str
     ) -> jeepney.low_level.Message:
@@ -171,6 +176,11 @@ def schedule_shutdown(*, action: str, delay: datetime.timedelta) -> None:
     _log_shutdown_inhibitors(login_manager)
 
 
+def suspend() -> None:
+    _LOGGER.info("suspending system")
+    get_login_manager_proxy().Suspend(interactive=False)
+
+
 def lock_all_sessions() -> None:
     """
     $ loginctl lock-sessions

+ 1 - 0
tests/dbus/message-generators/test_login_manager.py

@@ -51,6 +51,7 @@ def mock_open_dbus_connection() -> typing.Iterator[unittest.mock.MagicMock]:
             },
             ("poweroff", 0),
         ),
+        ("Suspend", "b", {"interactive": True}, (True,)),
         (
             "Inhibit",
             "ssss",

+ 10 - 0
tests/test_action.py

@@ -64,3 +64,13 @@ def test_mqtt_topic_suffix_action_mapping_lock():
     ):
         mqtt_action.trigger(state="dummy")
     login_manager_mock.LockSessions.assert_called_once_with()
+
+
+def test_mqtt_topic_suffix_action_mapping_suspend():
+    mqtt_action = systemctl_mqtt._MQTT_TOPIC_SUFFIX_ACTION_MAPPING["suspend"]
+    login_manager_mock = unittest.mock.MagicMock()
+    with unittest.mock.patch(
+        "systemctl_mqtt._dbus.get_login_manager_proxy", return_value=login_manager_mock
+    ):
+        mqtt_action.trigger(state="dummy")
+    login_manager_mock.Suspend.assert_called_once_with(interactive=False)

+ 12 - 0
tests/test_dbus.py

@@ -151,6 +151,18 @@ def test__schedule_shutdown_fail(
     assert "inhibitor" in caplog.records[2].message
 
 
+def test_suspend(caplog):
+    login_manager_mock = unittest.mock.MagicMock()
+    with unittest.mock.patch(
+        "systemctl_mqtt._dbus.get_login_manager_proxy", return_value=login_manager_mock
+    ), caplog.at_level(logging.INFO):
+        systemctl_mqtt._dbus.suspend()
+    login_manager_mock.Suspend.assert_called_once_with(interactive=False)
+    assert len(caplog.records) == 1
+    assert caplog.records[0].levelno == logging.INFO
+    assert caplog.records[0].message == "suspending system"
+
+
 def test_lock_all_sessions(caplog):
     login_manager_mock = unittest.mock.MagicMock()
     with unittest.mock.patch(

+ 3 - 2
tests/test_mqtt.py

@@ -124,6 +124,7 @@ def test__run(
     assert sorted(mqtt_subscribe_mock.call_args_list) == [
         unittest.mock.call(mqtt_topic_prefix + "/lock-all-sessions"),
         unittest.mock.call(mqtt_topic_prefix + "/poweroff"),
+        unittest.mock.call(mqtt_topic_prefix + "/suspend"),
     ]
     assert mqtt_client.on_message is None
     for suffix in ("poweroff", "lock-all-sessions"):
@@ -156,13 +157,13 @@ def test__run(
     assert all(r.levelno == logging.INFO for r in caplog.records[4::2])
     assert {r.message for r in caplog.records[4::2]} == {
         f"subscribing to {mqtt_topic_prefix}/{s}"
-        for s in ("poweroff", "lock-all-sessions")
+        for s in ("poweroff", "lock-all-sessions", "suspend")
     }
     assert all(r.levelno == logging.DEBUG for r in caplog.records[5::2])
     assert {r.message for r in caplog.records[5::2]} == {
         f"registered MQTT callback for topic {mqtt_topic_prefix}/{s}"
         f" triggering {systemctl_mqtt._MQTT_TOPIC_SUFFIX_ACTION_MAPPING[s]}"
-        for s in ("poweroff", "lock-all-sessions")
+        for s in ("poweroff", "lock-all-sessions", "suspend")
     }
     open_dbus_connection_mock.return_value.filter.assert_called_once()
     # waited for mqtt loop to stop?

+ 7 - 0
tests/test_state_dbus.py

@@ -167,5 +167,12 @@ def test_publish_homeassistant_device_config(
                 "platform": "button",
                 "command_topic": f"{topic_prefix}/lock-all-sessions",
             },
+            "logind/suspend": {
+                "unique_id": f"systemctl-mqtt-{hostname}-logind-suspend",
+                "object_id": f"{hostname}_logind_suspend",
+                "name": "suspend",
+                "platform": "button",
+                "command_topic": f"{topic_prefix}/suspend",
+            },
         },
     }