Files
POS-Automation/pos_briefing_mqtt.py
T
Mortdecai b9865be55f fix: direct-IP fallback when printer_mac unset; force utf-8 in bridge subprocess
Final-review hardening:
- discover_printer falls back to direct IP reachability when printer_mac is
  empty (restores legacy behavior; avoids permanent unreachable on misconfig).
  Keeps the MAC check when a MAC IS set so we never print to a wrong DHCP tenant.
- bridge subprocess decodes child stdout as utf-8 explicitly, so a LANG=C systemd
  locale can't UnicodeDecodeError on the success checkmark and mis-report error.
- add tests for both no-MAC fallback paths (5 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:48:59 -04:00

129 lines
4.6 KiB
Python

#!/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, encoding="utf-8",
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():
# paho 2.x requires an explicit CallbackAPIVersion. We use VERSION1 to match
# our (client, userdata, flags, rc) callback signatures and the working lgtv
# bridge. paho logs a one-time deprecation warning for v1 at startup; it is
# harmless and does not affect operation.
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()