From 980002064a499f0973a923e76e035b98c2c5acda Mon Sep 17 00:00:00 2001 From: Mortdecai Date: Sun, 7 Jun 2026 13:37:41 -0400 Subject: [PATCH] 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) --- pos_briefing_mqtt.py | 125 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 pos_briefing_mqtt.py diff --git a/pos_briefing_mqtt.py b/pos_briefing_mqtt.py new file mode 100644 index 0000000..218352d --- /dev/null +++ b/pos_briefing_mqtt.py @@ -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: +""" +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()