From 204e0a1fbc3217aa146efae9e5f302dfcffeb303 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" <git@edgarpierre.fr> Date: Sun, 9 Mar 2025 14:41:11 +0100 Subject: [PATCH] Add volume control and state publishing to HassUserClient --- config.example.toml | 1 + hasspy/mqtt.py | 92 +++++++++++++++++++++++++++++++++------------ 2 files changed, 68 insertions(+), 25 deletions(-) diff --git a/config.example.toml b/config.example.toml index 3d69da9..033019a 100644 --- a/config.example.toml +++ b/config.example.toml @@ -4,3 +4,4 @@ username = "hasspy" password = "password" log_level = "INFO" +interval = 60 diff --git a/hasspy/mqtt.py b/hasspy/mqtt.py index 3f70a2b..00d9487 100644 --- a/hasspy/mqtt.py +++ b/hasspy/mqtt.py @@ -1,6 +1,7 @@ import json import logging from subprocess import run +from threading import Timer from typing import Any, Mapping from paho.mqtt.client import Client, MQTTMessage, MQTTMessageInfo @@ -20,7 +21,9 @@ class HassClient(Client): if username: self.username_pw_set(username, self.config.get("password")) + self.interval = self.config.get("interval", 60) self.power_on = True + self.timer = Timer(self.interval, self.publish_state) self.connect() @@ -55,6 +58,11 @@ class HassClient(Client): self.message_callback_add(self.command_topic, self.on_command) def publish_state(self) -> MQTTMessageInfo: + self.timer.cancel() + self.timer = Timer(self.interval, self.publish_state) + self.timer.start() + + log.debug("Publishing state message") return self.publish_json( topic=self.state_topic, payload=self.state_payload, @@ -64,12 +72,12 @@ class HassClient(Client): payload = message.payload.decode("utf-8") log.debug(f"Received command: {payload}") - self.do_command(payload) + self.do_command(*payload.split(":")) self.publish_state() - def do_command(self, payload: str) -> None: - log.debug(f"Executing command: {payload}") + def do_command(self, cmd: str, value: str = "") -> None: + pass def on_connect(self, *args: Any, **kwargs: Any) -> None: log.info("Connected to MQTT broker") @@ -116,7 +124,7 @@ class HassClient(Client): return {} @property - def components(self) -> dict[str, dict[str, str]]: + def components(self) -> dict[str, dict[str, Any]]: return {} @@ -127,23 +135,20 @@ class HassSystemClient(HassClient): "LOCK": ["loginctl", "lock-sessions"], } - def do_command(self, payload: str) -> None: - if payload not in self.commands: - return + def do_command(self, cmd: str, value: str = "") -> None: + if cmd in self.commands: + log.debug(f"Executing command: {cmd}") + proc = run(self.commands[cmd]) + if proc.returncode != 0: + log.error(f"Failed to execute command: {cmd}") - super().do_command(payload) - - proc = run(self.commands[payload]) - if proc.returncode != 0: - log.error(f"Failed to execute command: {payload}") - - if payload == "POWER_ON": + if cmd == "POWER_ON": self.power_on = True - elif payload == "POWER_OFF": + elif cmd == "POWER_OFF": self.power_on = False @property - def components(self) -> dict[str, dict[str, str]]: + def components(self) -> dict[str, dict[str, Any]]: return { "power": { "unique_id": f"{self.node_id}_power", @@ -176,22 +181,33 @@ class HassUserClient(HassClient): def __init__(self, node_id: str, config: Mapping[str, Any]) -> None: super().__init__(f"{node_id}", config) - def do_command(self, payload: str) -> None: - if payload not in self.commands: - return + def do_command(self, cmd: str, value: str = "") -> None: + if cmd in self.commands: + log.debug(f"Executing command: {cmd}") + proc = run(self.commands[cmd]) + if proc.returncode != 0: + log.error(f"Failed to execute command: {cmd}") - super().do_command(payload) - - proc = run(self.commands[payload]) - if proc.returncode != 0: - log.error(f"Failed to execute command: {payload}") + match [cmd, value]: + case ["VOLUME", value]: + log.debug(f"Executing command: {cmd}:{value}") + proc = run( + [ + "wpctl", + "set-volume", + "@DEFAULT_AUDIO_SINK@", + f"{int(value) / 100:.2f}", + ] + ) + if proc.returncode != 0: + log.error(f"Failed to set volume: {value}") @property def availability_topic(self) -> str: return f"{self.node_id}/user/availability" @property - def components(self) -> dict[str, dict[str, str]]: + def components(self) -> dict[str, dict[str, Any]]: return { "play-pause": { "unique_id": f"{self.node_id}_play_pause", @@ -199,4 +215,30 @@ class HassUserClient(HassClient): "name": "Play/Pause", "payload_press": "PLAY_PAUSE", }, + "volume": { + "unique_id": f"{self.node_id}_volume", + "p": "number", + "name": "Volume", + "command_template": "VOLUME:{{ value }}", + "step": 10, + "min": 0, + "max": 100, + "unit_of_measurement": "%", + "value_template": "{{ value_json.volume }}", + }, } + + @property + def state_payload(self) -> dict[str, Any]: + return { + "volume": self.volume_value, + } + + @property + def volume_value(self) -> int: + proc = run(["wpctl", "get-volume", "@DEFAULT_AUDIO_SINK@"], capture_output=True) + if proc.returncode != 0: + log.error("Failed to get volume") + return 0 + + return int(float(proc.stdout.decode("utf-8").split(": ")[1]) * 100)