From 77aa8b5b926e910cc6a1a4634836602293f15bdf Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" <git@edgarpierre.fr> Date: Sun, 9 Mar 2025 12:26:57 +0100 Subject: [PATCH] Add user mode support with separate client classes for system and user modes --- hasspy/__main__.py | 18 +++++-- hasspy/mqtt.py | 116 ++++++++++++++++++++++++++++++++------------- 2 files changed, 97 insertions(+), 37 deletions(-) diff --git a/hasspy/__main__.py b/hasspy/__main__.py index a4e0c53..2d63178 100644 --- a/hasspy/__main__.py +++ b/hasspy/__main__.py @@ -3,7 +3,7 @@ import tomllib from argparse import ArgumentParser from pathlib import Path -from hasspy.mqtt import HassClient +from hasspy.mqtt import HassClient, HassSystemClient, HassUserClient def main() -> None: @@ -15,6 +15,7 @@ def main() -> None: 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: @@ -41,10 +42,17 @@ def main() -> None: level=config.get("log_level", logging.INFO) - (args.verbose * 10) ) - ha = HassClient( - "orchomenos", - config=config, - ) + ha: HassClient + if not args.user: + ha = HassSystemClient( + "orchomenos", + config=config, + ) + else: + ha = HassUserClient( + "orchomenos", + config=config, + ) ha.loop_forever() diff --git a/hasspy/mqtt.py b/hasspy/mqtt.py index 4421384..dd23d48 100644 --- a/hasspy/mqtt.py +++ b/hasspy/mqtt.py @@ -1,3 +1,4 @@ +import getpass import json import logging from subprocess import run @@ -16,11 +17,6 @@ class HassClient(Client): self.node_id = node_id self.config = config - self.state_topic = f"{self.node_id}/state" - self.availability_topic = f"{self.node_id}/availability" - self.command_topic = f"{self.node_id}/set" - self.discovery_topic = f"homeassistant/device/{self.node_id}/config" - username = self.config.get("username") if username: self.username_pw_set(username, self.config.get("password")) @@ -62,11 +58,20 @@ class HassClient(Client): def publish_state(self) -> MQTTMessageInfo: return self.publish_json( topic=self.state_topic, - payload={ - "power": "POWER_ON" if self.power_on else "POWER_OFF", - }, + payload=self.state_payload, ) + def on_command(self, client: Client, userdata: Any, message: MQTTMessage) -> None: + payload = message.payload.decode("utf-8") + log.debug(f"Received command: {payload}") + + self.do_command(payload) + + self.publish_state() + + def do_command(self, payload: str) -> None: + log.debug(f"Executing command: {payload}") + def on_connect(self, *args: Any, **kwargs: Any) -> None: log.info("Connected to MQTT broker") self.publish_discovery() @@ -75,9 +80,50 @@ class HassClient(Client): self.publish_state() - def on_command(self, client: Client, userdata: Any, message: MQTTMessage) -> None: - payload = message.payload.decode("utf-8") - log.debug(f"Received command: {payload}") + @property + def state_topic(self) -> str: + return f"{self.node_id}/state" + + @property + def availability_topic(self) -> str: + return f"{self.node_id}/availability" + + @property + def command_topic(self) -> str: + return f"{self.node_id}/set" + + @property + def discovery_topic(self) -> str: + return f"homeassistant/device/{self.node_id}/config" + + @property + def discovery_payload(self) -> dict[str, Any]: + return { + "dev": { + "ids": self.node_id, + "name": self.node_id, + }, + "o": { + "name": "hasspy", + }, + "cmps": self.components, + "availability_topic": self.availability_topic, + "command_topic": self.command_topic, + "state_topic": self.state_topic, + } + + @property + def state_payload(self) -> dict[str, Any]: + return {} + + @property + def components(self) -> dict[str, dict[str, str]]: + return {} + + +class HassSystemClient(HassClient): + def do_command(self, payload: str) -> None: + super().do_command(payload) match payload: case "POWER_ON": @@ -96,30 +142,36 @@ class HassClient(Client): proc = run(["systemctl", "poweroff", "--when=+1m"]) if proc.returncode != 0: log.error("Failed to schedule shutdown") - - self.publish_state() + case "LOCK": + log.info("Locking screen…") + run(["loginctl", "lock-sessions"]) @property - def discovery_payload(self) -> dict[str, Any]: + def components(self) -> dict[str, dict[str, str]]: return { - "dev": { - "ids": self.node_id, - "name": self.node_id, + "power": { + "unique_id": f"{self.node_id}_power", + "p": "switch", + "name": "Power", + "payload_off": "POWER_OFF", + "payload_on": "POWER_ON", + "value_template": "{{ value_json.power }}", }, - "o": { - "name": "hasspy", + "lock": { + "unique_id": f"{self.node_id}_power", + "p": "button", + "name": "Lock", + "payload_press": "LOCK", }, - "cmps": { - "power": { - "unique_id": f"{self.node_id}_power", - "p": "switch", - "name": "Power", - "payload_off": "POWER_OFF", - "payload_on": "POWER_ON", - "value_template": "{{ value_json.power }}", - }, - }, - "availability_topic": self.availability_topic, - "command_topic": self.command_topic, - "state_topic": self.state_topic, } + + @property + def state_payload(self) -> dict[str, Any]: + return { + "power": self.power_on, + } + + +class HassUserClient(HassClient): + def __init__(self, node_id: str, config: Mapping[str, Any]) -> None: + super().__init__(f"{node_id}_{getpass.getuser()}", config)