b9865be55f
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>
129 lines
4.6 KiB
Python
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()
|