Add volume control and state publishing to HassUserClient

This commit is contained in:
Edgar P. Burkhart 2025-03-09 14:41:11 +01:00
parent b5a3846cc5
commit 204e0a1fbc
Signed by: edpibu
GPG key ID: 9833D3C5A25BD227
2 changed files with 68 additions and 25 deletions

View file

@ -4,3 +4,4 @@ username = "hasspy"
password = "password"
log_level = "INFO"
interval = 60

View file

@ -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)