cb7ad8b0ff
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>
121 lines
6.8 KiB
Markdown
121 lines
6.8 KiB
Markdown
# 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).
|