docs: design for HA/Google Home on-demand POS briefing trigger
Replaces the never-installed cron approach with an MQTT bridge mirroring the lgtv pattern: Google Home button -> HA script/scene -> MQTT -> steel141 prints. Adds MAC-based printer discovery (printer IP is DHCP-unstable) and a gemma4 summarizer swap. Includes scripts/dryrun.py (non-printing pipeline harness). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
# POS Briefing — Home Assistant / Google Home Trigger (Design)
|
||||
|
||||
Date: 2026-06-07
|
||||
Status: Proposed — awaiting Seth's review
|
||||
|
||||
## Goal
|
||||
|
||||
Replace the (never-installed) cron schedule with an **on-demand** trigger: tap a
|
||||
**button in Google Home** → the daily briefing prints on the Epson TM-m30. No cron.
|
||||
|
||||
## Background / findings (this session)
|
||||
|
||||
- "Not firing" had two causes, both now moot under this design: (1) no cron entry
|
||||
was ever installed on steel141 — the "schedule 4:58am/3pm" commit only edited
|
||||
SESSION.md; (2) `yfinance` was missing from steel141's system python3 →
|
||||
**installed** (`pip --user --break-system-packages`, matching lgtv's PEP-668 approach).
|
||||
- The pipeline itself **works end-to-end on steel141** (verified via non-printing
|
||||
dry run, `scripts/dryrun.py`): FreshRSS 100 items, weather (the uncommitted
|
||||
hourly rewrite), finance (yfinance), and summarization all produce a ~2.4 KB
|
||||
ESC/POS receipt. `zfs`/`reddit`/`grafana` return `None` on steel141 (non-fatal;
|
||||
out of scope here — see Out of Scope).
|
||||
- Printer **changes IP on every restart** (router DHCP, no reservation). Currently
|
||||
`192.168.0.184:9100`, MAC `50:57:9c:eb:0f:07` (Epson). Always on.
|
||||
|
||||
## Architecture
|
||||
|
||||
Mirrors the proven `lgtv` chain (steel141 systemd MQTT bridge + HA MQTT discovery +
|
||||
free manual `google_assistant` integration).
|
||||
|
||||
```
|
||||
[Google Home tap]
|
||||
→ google_assistant integration (already wired; acct slingshooter08; exposes LG TV switch today)
|
||||
→ HA script "Print Daily Briefing" (exposed to Google as a Scene → momentary button)
|
||||
→ MQTT publish to broker CT 301 (192.168.0.154:1883)
|
||||
→ pos-briefing-mqtt.service on steel141 (systemd, User=claude)
|
||||
→ pos_briefing.py → printer discovery (by MAC) → ESC/POS over TCP :9100
|
||||
→ status/availability published back to MQTT (HA shows success/fail)
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### 1. MQTT bridge — `pos_briefing_mqtt.py` + `pos-briefing-mqtt.service` (new, steel141)
|
||||
- systemd `Type=simple`, `User=claude`, `EnvironmentFile=/etc/pos-briefing/pos-briefing.env`
|
||||
(holds `MQTT_PASSWORD`, root-owned 0600, never committed). `Restart=on-failure`.
|
||||
- Connects to broker `192.168.0.154:1883`, user `posbriefing` (or reuse an existing
|
||||
broker user — TBD at install; lgtv uses a per-device user).
|
||||
- On connect: publishes **MQTT discovery** for an HA **button** entity
|
||||
(`homeassistant/button/pos_briefing/config`) + availability `online`; LWT → `offline`.
|
||||
- Subscribes command topic `posbriefing/button/press` (payload `PRESS`).
|
||||
- On press: runs the briefing **as a subprocess**
|
||||
(`/usr/bin/python3 /home/claude/bin/POS-Automation/pos_briefing.py`) so a slow/long
|
||||
print never blocks the MQTT loop and a script crash can't kill the bridge.
|
||||
Publishes `posbriefing/status` = `printing` → `ok` / `error:<msg>`.
|
||||
- Debounce: ignore a new press while one is already in flight (prevents double prints).
|
||||
|
||||
Topics:
|
||||
| Topic | Dir | Payload |
|
||||
|-------|-----|---------|
|
||||
| `homeassistant/button/pos_briefing/config` | pub (retain) | discovery JSON |
|
||||
| `posbriefing/availability` | pub (retain) | `online` / `offline` (LWT) |
|
||||
| `posbriefing/button/press` | sub | `PRESS` |
|
||||
| `posbriefing/status` | pub (retain) | `idle` / `printing` / `ok` / `error:<msg>` |
|
||||
|
||||
### 2. Printer discovery (new, in `pos_briefing.py`) — kills the DHCP-IP problem
|
||||
- Resolve current printer IP by **MAC `50:57:9c:eb:0f:07`** instead of trusting a
|
||||
hardcoded `printer_ip`.
|
||||
- Algorithm (lgtv "cached IP, sweep on miss"):
|
||||
1. Read cached IP from `~/.cache/pos-briefing/last_ip` (seed = config `printer_ip`).
|
||||
2. `ping -c1` it, check `ip neigh` lladdr == target MAC, and `:9100` open → use it.
|
||||
3. On miss: ARP-prime sweep the /24 (`ping -c1 -W1` fan-out), then scan `ip neigh`
|
||||
for the MAC → its IP. Re-cache.
|
||||
4. Still not found → raise a clear error (bridge reports `error:printer-not-found`).
|
||||
- Config gains `printer_mac`; `printer_ip` becomes a seed/fallback only.
|
||||
- **Belt-and-suspenders (Seth's action, router-side):** add a DHCP reservation for
|
||||
the printer MAC so the IP stops moving. Discovery makes this optional for correctness.
|
||||
|
||||
### 3. HA script + Google Home exposure (Seth's HA side; I provide exact YAML/steps)
|
||||
- MQTT discovery auto-creates `button.pos_briefing` (no hand YAML for the entity).
|
||||
- Google's `google_assistant` integration doesn't support HA `button` entities → wrap:
|
||||
an HA `script.print_daily_briefing` that presses the button. Scripts map to Google's
|
||||
**Scene** trait → appear as a tappable **button** in the Google Home app (momentary).
|
||||
- Add the script to the same `google_assistant` expose list that already exposes the
|
||||
TV switch; re-sync. (Per lgtv DECISIONS 2026-06-06: manual integration, scopes must
|
||||
include email+name, "HTTP basic auth header" OFF, Test activated by slingshooter08.)
|
||||
|
||||
### 4. gemma4 summarizer swap (in `pos_briefing.py`)
|
||||
- Point summarization at a **gemma4 model on steel141 Ollama** (candidate
|
||||
`gemma4:26b` or `gemma4:31b-it-q4_K_M`) via the existing direct Ollama HTTP call —
|
||||
change `ollama_model` in config + the prompt string.
|
||||
- **Prompt convergence:** done by Claude *now*, interactively, via the `ask_gemma4`
|
||||
MCP tool against real FreshRSS article samples, iterating until summaries are tight
|
||||
(30–50 words, factual, no preamble). The finalized prompt string is then baked into
|
||||
`pos_briefing.py` (which calls Ollama directly — it cannot call the MCP at runtime).
|
||||
|
||||
### 5. Deps / install
|
||||
- `yfinance` — installed (done). `paho-mqtt` — present for `claude`. Confirm both
|
||||
pinned/persisted. `install.sh` mirrors lgtv: copies bridge to `/usr/local/bin`,
|
||||
unit to `/etc/systemd/system`, env to `/etc/pos-briefing/`, `daemon-reload`, enable.
|
||||
|
||||
## Error handling
|
||||
- Printer not found / unreachable → `error:printer-not-found`; no partial print.
|
||||
- Briefing subprocess nonzero exit or timeout (cap ~180s) → `error:<rc>`; bridge stays up.
|
||||
- MQTT disconnect → paho auto-reconnect; LWT marks `offline` so HA/Google show it down.
|
||||
- Data-source failures inside the briefing remain non-fatal (dashboard prints what it has).
|
||||
|
||||
## Testing
|
||||
- `scripts/dryrun.py` — full pipeline, print suppressed (already used to verify).
|
||||
- Bridge unit: publish `PRESS` to the command topic by hand (`mosquitto_pub`) → observe
|
||||
`status` transitions and a real receipt; confirm `button.pos_briefing` appears in HA.
|
||||
- Discovery: unit-test MAC→IP resolution with cache hit, cache miss+sweep, and not-found.
|
||||
- End-to-end: tap the button in Google Home → receipt prints.
|
||||
|
||||
## Out of scope (noted, not fixed here)
|
||||
- `zfs` (steel141 isn't the tank host — `cannot open 'tank'`), `reddit`, `grafana`
|
||||
returning `None` on steel141. Track separately.
|
||||
- Committing the in-tree weather rewrite is folded into this work (it's verified).
|
||||
|
||||
## Open items for Seth
|
||||
- Broker MQTT user for the bridge: new `posbriefing` user vs reuse existing? (install-time)
|
||||
- gemma4 model choice: `26b` vs `31b-it-q4_K_M` (I'll recommend after convergence).
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dry-run harness: exercises the full pos_briefing pipeline WITHOUT printing.
|
||||
|
||||
Monkeypatches print_receipt so nothing is sent to the TM-m30 over TCP. Reports
|
||||
which data sources succeeded/failed and the receipt byte length. Use to verify
|
||||
the script runs end-to-end on a host before scheduling it via cron.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import pos_briefing as pb
|
||||
|
||||
|
||||
def fake_print_receipt(raw):
|
||||
print(f"[dry-run] print_receipt() suppressed — {len(raw)} bytes would be sent "
|
||||
f"to {pb.PRINTER_IP}:{pb.PRINTER_PORT}")
|
||||
return True
|
||||
|
||||
|
||||
pb.print_receipt = fake_print_receipt
|
||||
|
||||
|
||||
def probe(name, fn):
|
||||
try:
|
||||
v = fn()
|
||||
ok = v is not None and v != [] and v != {}
|
||||
print(f" {'OK ' if ok else 'NIL'} {name}: {repr(v)[:120]}")
|
||||
return v
|
||||
except Exception as e:
|
||||
print(f" ERR {name}: {type(e).__name__}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
print("=== data source probes ===")
|
||||
items, token = (pb.fetch_news() if True else (None, None))
|
||||
print(f" news: {len(items) if items else 0} items, token={'yes' if token else 'no'}")
|
||||
zfs = probe("zfs", pb.get_zfs_status)
|
||||
reddit = probe("reddit", pb.get_reddit_top)
|
||||
grafana = probe("grafana", pb.get_grafana_metrics)
|
||||
weather = probe("weather", pb.get_weather)
|
||||
finance = probe("finance", pb.get_financial_snapshot)
|
||||
|
||||
print("=== build + (suppressed) print ===")
|
||||
articles = pb.summarize_news(items, count=5, length="30-50 words") if items else []
|
||||
print(f" articles summarized: {len(articles)}")
|
||||
raw = pb.build_receipt(articles, zfs, reddit, grafana, weather, finance)
|
||||
pb.print_receipt(raw)
|
||||
print("=== done (nothing printed) ===")
|
||||
Reference in New Issue
Block a user