feat: MQTT bridge exposing an HA button to print the briefing
Subscribes to posbriefing/button/press; on PRESS runs pos_briefing.py as a subprocess (debounced via a lock) and reports status/availability. Registers an HA button via MQTT discovery. Pins paho CallbackAPIVersion.VERSION1 (paho 2.1.0 is installed for claude and warns on the implicit default). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""POS Briefing MQTT bridge for Home Assistant.
|
||||
|
||||
Runs on steel141 as user `claude` via systemd. Subscribes to a command topic;
|
||||
on PRESS, runs pos_briefing.py as a subprocess and prints the daily briefing.
|
||||
Registers an HA button via MQTT discovery and reports status/availability.
|
||||
|
||||
Topics:
|
||||
homeassistant/button/pos_briefing/config -- discovery (retained, on connect)
|
||||
posbriefing/availability -- online / offline (LWT)
|
||||
posbriefing/button/press -- receives: PRESS
|
||||
posbriefing/status -- idle / printing / ok / error:<msg>
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import subprocess
|
||||
import threading
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
MQTT_HOST = os.environ.get("MQTT_HOST", "192.168.0.154")
|
||||
MQTT_PORT = int(os.environ.get("MQTT_PORT", "1883"))
|
||||
MQTT_USER = os.environ.get("MQTT_USER", "posbriefing")
|
||||
MQTT_PASS = os.environ.get("MQTT_PASSWORD", "")
|
||||
|
||||
BRIEFING = "/home/claude/bin/POS-Automation/pos_briefing.py"
|
||||
PRINT_TIMEOUT = 180
|
||||
|
||||
DISCOVERY_TOPIC = "homeassistant/button/pos_briefing/config"
|
||||
AVAIL_TOPIC = "posbriefing/availability"
|
||||
COMMAND_TOPIC = "posbriefing/button/press"
|
||||
STATUS_TOPIC = "posbriefing/status"
|
||||
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format="%(asctime)s [pos-mqtt] %(levelname)s %(message)s")
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_busy = threading.Lock()
|
||||
|
||||
|
||||
def publish_discovery(client):
|
||||
payload = {
|
||||
"name": "Print Daily Briefing",
|
||||
"unique_id": "pos_briefing_button",
|
||||
"command_topic": COMMAND_TOPIC,
|
||||
"payload_press": "PRESS",
|
||||
"availability_topic": AVAIL_TOPIC,
|
||||
"payload_available": "online",
|
||||
"payload_not_available": "offline",
|
||||
"device": {
|
||||
"identifiers": ["pos_briefing"],
|
||||
"name": "POS Briefing",
|
||||
"manufacturer": "Seth",
|
||||
"model": "TM-m30 briefing",
|
||||
},
|
||||
"icon": "mdi:printer-pos",
|
||||
}
|
||||
client.publish(DISCOVERY_TOPIC, json.dumps(payload), retain=True)
|
||||
client.publish(AVAIL_TOPIC, "online", retain=True)
|
||||
client.publish(STATUS_TOPIC, "idle", retain=True)
|
||||
log.info("Published MQTT discovery payload")
|
||||
|
||||
|
||||
def run_briefing(client):
|
||||
if not _busy.acquire(blocking=False):
|
||||
log.warning("Briefing already running; ignoring press")
|
||||
return
|
||||
try:
|
||||
client.publish(STATUS_TOPIC, "printing", retain=True)
|
||||
log.info("Running briefing...")
|
||||
r = subprocess.run(["/usr/bin/python3", BRIEFING],
|
||||
capture_output=True, text=True, timeout=PRINT_TIMEOUT)
|
||||
if r.returncode == 0 and "Receipt sent" in r.stdout:
|
||||
client.publish(STATUS_TOPIC, "ok", retain=True)
|
||||
log.info("Briefing printed OK")
|
||||
else:
|
||||
tail = (r.stdout + r.stderr).strip().splitlines()[-1:] or ["nonzero"]
|
||||
client.publish(STATUS_TOPIC, f"error:{tail[0][:80]}", retain=True)
|
||||
log.error(f"Briefing failed rc={r.returncode}: {tail}")
|
||||
except subprocess.TimeoutExpired:
|
||||
client.publish(STATUS_TOPIC, "error:timeout", retain=True)
|
||||
log.error("Briefing timed out")
|
||||
finally:
|
||||
_busy.release()
|
||||
|
||||
|
||||
def on_connect(client, userdata, flags, rc):
|
||||
if rc == 0:
|
||||
log.info("Connected to MQTT broker")
|
||||
publish_discovery(client)
|
||||
client.subscribe(COMMAND_TOPIC)
|
||||
log.info(f"Subscribed to {COMMAND_TOPIC}")
|
||||
else:
|
||||
log.error(f"MQTT connect failed: rc={rc}")
|
||||
|
||||
|
||||
def on_message(client, userdata, msg):
|
||||
cmd = msg.payload.decode().strip().upper()
|
||||
log.info(f"Received: {cmd}")
|
||||
if cmd == "PRESS":
|
||||
threading.Thread(target=run_briefing, args=(client,), daemon=True).start()
|
||||
else:
|
||||
log.warning(f"Unknown command: {cmd}")
|
||||
|
||||
|
||||
def main():
|
||||
# Explicit VERSION1 callbacks (paho 2.x defaults to this but warns); our
|
||||
# on_connect/on_message use the (client, userdata, flags, rc) signature.
|
||||
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1,
|
||||
client_id="pos-briefing-bridge")
|
||||
client.username_pw_set(MQTT_USER, MQTT_PASS)
|
||||
client.on_connect = on_connect
|
||||
client.on_message = on_message
|
||||
client.will_set(AVAIL_TOPIC, "offline", retain=True)
|
||||
log.info(f"Connecting to {MQTT_HOST}:{MQTT_PORT}...")
|
||||
client.connect(MQTT_HOST, MQTT_PORT, keepalive=60)
|
||||
try:
|
||||
client.loop_forever()
|
||||
finally:
|
||||
client.publish(AVAIL_TOPIC, "offline", retain=True)
|
||||
client.disconnect()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user