From fad234ad009e5119138c8d6f79696e219d3f6632 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart" <git@edgarpierre.fr>
Date: Sun, 9 Mar 2025 12:59:30 +0100
Subject: [PATCH] Refactor main entry point and command handling in
 HassSystemClient and HassUserClient

---
 hasspy/__init__.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++
 hasspy/__main__.py | 62 ++----------------------------------------
 hasspy/mqtt.py     | 56 +++++++++++++++++++++-----------------
 pyproject.toml     |  3 ++
 4 files changed, 104 insertions(+), 85 deletions(-)

diff --git a/hasspy/__init__.py b/hasspy/__init__.py
index e69de29..8a97f14 100644
--- a/hasspy/__init__.py
+++ b/hasspy/__init__.py
@@ -0,0 +1,68 @@
+import logging
+import signal
+import tomllib
+from argparse import ArgumentParser
+from pathlib import Path
+
+from hasspy.mqtt import HassClient, HassSystemClient, HassUserClient
+
+log = logging.getLogger(__name__)
+
+
+def main() -> int:
+    log.info("Starting HassPy")
+    parser = ArgumentParser(
+        prog="HassPy",
+        description="Home Assistant MQTT client",
+    )
+    parser.add_argument("-c", "--config", help="Path to configuration file")
+    parser.add_argument(
+        "-v", "--verbose", help="Enable verbose logging", action="count", default=0
+    )
+    parser.add_argument("-u", "--user", help="User mode client", action="store_true")
+    args = parser.parse_args()
+
+    if args.config:
+        config_file = Path(args.config)
+    else:
+        config_file = next(
+            (
+                x
+                for x in (Path("/etc/hasspy.toml"), Path("/etc/hasspy/config.toml"))
+                if x.exists()
+            ),
+            Path("config.toml"),
+        )
+
+    if not config_file or not config_file.exists():
+        raise FileNotFoundError("No configuration file found")
+
+    with open(config_file, "rb") as file:
+        config = tomllib.load(file)
+
+    if isinstance(config.get("log_level"), str):
+        config["log_level"] = getattr(logging, config["log_level"])
+    logging.basicConfig(
+        level=config.get("log_level", logging.INFO) - (args.verbose * 10)
+    )
+
+    ha: HassClient
+    if not args.user:
+        ha = HassSystemClient(
+            "orchomenos",
+            config=config,
+        )
+    else:
+        ha = HassUserClient(
+            "orchomenos",
+            config=config,
+        )
+
+    ha.loop_start()
+
+    signal.sigwait([signal.SIGHUP, signal.SIGINT, signal.SIGTERM])
+
+    log.info("Shutting down")
+
+    ha.loop_stop()
+    return 0
diff --git a/hasspy/__main__.py b/hasspy/__main__.py
index 2d63178..ab01058 100644
--- a/hasspy/__main__.py
+++ b/hasspy/__main__.py
@@ -1,61 +1,3 @@
-import logging
-import tomllib
-from argparse import ArgumentParser
-from pathlib import Path
+from hasspy import main
 
-from hasspy.mqtt import HassClient, HassSystemClient, HassUserClient
-
-
-def main() -> None:
-    parser = ArgumentParser(
-        prog="HassPy",
-        description="Home Assistant MQTT client",
-    )
-    parser.add_argument("-c", "--config", help="Path to configuration file")
-    parser.add_argument(
-        "-v", "--verbose", help="Enable verbose logging", action="count", default=0
-    )
-    parser.add_argument("-u", "--user", help="User mode client", action="store_true")
-    args = parser.parse_args()
-
-    if args.config:
-        config_file = Path(args.config)
-    else:
-        config_file = next(
-            (
-                x
-                for x in (Path("/etc/hasspy.toml"), Path("/etc/hasspy/config.toml"))
-                if x.exists()
-            ),
-            Path("config.toml"),
-        )
-
-    if not config_file or not config_file.exists():
-        raise FileNotFoundError("No configuration file found")
-
-    with open(config_file, "rb") as file:
-        config = tomllib.load(file)
-
-    if isinstance(config.get("log_level"), str):
-        config["log_level"] = getattr(logging, config["log_level"])
-    logging.basicConfig(
-        level=config.get("log_level", logging.INFO) - (args.verbose * 10)
-    )
-
-    ha: HassClient
-    if not args.user:
-        ha = HassSystemClient(
-            "orchomenos",
-            config=config,
-        )
-    else:
-        ha = HassUserClient(
-            "orchomenos",
-            config=config,
-        )
-
-    ha.loop_forever()
-
-
-if __name__ == "__main__":
-    main()
+main()
diff --git a/hasspy/mqtt.py b/hasspy/mqtt.py
index e2d70ff..3f70a2b 100644
--- a/hasspy/mqtt.py
+++ b/hasspy/mqtt.py
@@ -1,4 +1,3 @@
-import getpass
 import json
 import logging
 from subprocess import run
@@ -122,29 +121,26 @@ class HassClient(Client):
 
 
 class HassSystemClient(HassClient):
+    commands = {
+        "POWER_ON": ["systemctl", "poweroff", "--when=cancel"],
+        "POWER_OFF": ["systemctl", "poweroff", "--when=+1m"],
+        "LOCK": ["loginctl", "lock-sessions"],
+    }
+
     def do_command(self, payload: str) -> None:
+        if payload not in self.commands:
+            return
+
         super().do_command(payload)
 
-        match payload:
-            case "POWER_ON":
-                if not self.power_on:
-                    log.info("Cancelling shutdown…")
-                    self.power_on = True
+        proc = run(self.commands[payload])
+        if proc.returncode != 0:
+            log.error(f"Failed to execute command: {payload}")
 
-                    proc = run(["systemctl", "poweroff", "--when=cancel"])
-                    if proc.returncode != 0:
-                        log.error("Failed to cancel shutdown")
-            case "POWER_OFF":
-                if self.power_on:
-                    log.info("Powering off…")
-                    self.power_on = False
-
-                    proc = run(["systemctl", "poweroff", "--when=+1m"])
-                    if proc.returncode != 0:
-                        log.error("Failed to schedule shutdown")
-            case "LOCK":
-                log.info("Locking screen…")
-                run(["loginctl", "lock-sessions"])
+        if payload == "POWER_ON":
+            self.power_on = True
+        elif payload == "POWER_OFF":
+            self.power_on = False
 
     @property
     def components(self) -> dict[str, dict[str, str]]:
@@ -173,16 +169,26 @@ class HassSystemClient(HassClient):
 
 
 class HassUserClient(HassClient):
+    commands = {
+        "PLAY_PAUSE": ["playerctl", "play-pause"],
+    }
+
     def __init__(self, node_id: str, config: Mapping[str, Any]) -> None:
-        super().__init__(f"{node_id}_{getpass.getuser()}", config)
+        super().__init__(f"{node_id}", config)
 
     def do_command(self, payload: str) -> None:
+        if payload not in self.commands:
+            return
+
         super().do_command(payload)
 
-        match payload:
-            case "PLAY_PAUSE":
-                log.info("Toggling play/pause…")
-                run(["playerctl", "play-pause"])
+        proc = run(self.commands[payload])
+        if proc.returncode != 0:
+            log.error(f"Failed to execute command: {payload}")
+
+    @property
+    def availability_topic(self) -> str:
+        return f"{self.node_id}/user/availability"
 
     @property
     def components(self) -> dict[str, dict[str, str]]:
diff --git a/pyproject.toml b/pyproject.toml
index b793eb6..6078950 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,3 +13,6 @@ dev = [
     "mypy>=1.15.0",
     "ruff>=0.9.10",
 ]
+
+[project.scripts]
+hasspy = "hasspy:main"