diff --git a/CONTEXT.md b/CONTEXT.md index ca807de..db29b64 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -45,10 +45,10 @@ Forked from `Seth/PrinterAutomation` on Gitea. - **Grafana:** `monitor.atermiter-x79.xyz` — Proxmox node metrics (173, 112, 197). - **ZFS:** SSH to `192.168.0.197` (node-197) — queries `zpool list tank`. - **Uptime Kuma:** CT 147 — queried via `pct exec 147` (requires Proxmox host). -- **This script runs on:** CT 166 (`192.168.0.175`, OpenClaw2). +- **This script runs on:** steel141 (`192.168.0.141`). Previously CT 166 (retired). ## Cron -Runs daily at 04:30 via crontab on CT 166: +Runs daily at 04:30 via crontab on steel141: ``` 30 4 * * * python3 /root/bin/POS-Automation/pos_briefing.py >> /var/log/pos_briefing.log 2>&1 ``` @@ -65,7 +65,7 @@ python3 pos_briefing.py ## Known Issues / Notes - `pct exec 147` (Uptime Kuma check) only works on a Proxmox host, not inside a CT — silently returns None. - `printer_ip` direct access (`192.168.0.137`) may be unreachable from some CTs due to Proxmox bridge/firewall state; pinging first resolves intermittent ARP issues. -- FreshRSS direct IP (`192.168.0.157`) can be intermittently unreachable from CT 166 — `fresh.sethpc.xyz` (via Caddy) works as fallback but adds latency. +- FreshRSS direct IP (`192.168.0.157`) can be intermittently unreachable — `fresh.sethpc.xyz` (via Caddy) works as fallback but adds latency. - yfinance ETH-USD occasionally fails with a NoneType error — non-fatal, other tickers still print. ## Gitea diff --git a/SESSION.md b/SESSION.md index d1b0f30..f570ab8 100644 --- a/SESSION.md +++ b/SESSION.md @@ -12,6 +12,12 @@ POS-only daily news briefing for the **Epson TM-m30** receipt printer (`192.168.0.137:9100`). Fetches unread articles from FreshRSS, selects and summarizes them using Ollama, collects dashboard data (weather, ZFS, Grafana, markets, Uptime Kuma), builds a native ESC/POS receipt via `python-escpos`, and sends it over TCP. Runs daily at 04:30 via cron on CT 166 (`192.168.0.175`). Forked and rewritten from `Seth/PrinterAutomation` — all PDF, CUPS, Telegram, and WhatsApp code removed. +## Memory Discipline + +- Update `SESSION.md` immediately when a durable fact, decision, or fix is discovered. +- Before every final reply, run a memory check and append any missing durable notes. +- End every reply with one line: `Session memory: updated` or `Session memory: no new durable facts.` + ## Gitea - Repo: `https://git.sethpc.xyz/Seth/POS-Automation` @@ -30,14 +36,14 @@ POS-only daily news briefing for the **Epson TM-m30** receipt printer (`192.168. - **No CLI flags** — parent script had `--POS` / `--pdfonly` / standard-mode paths. This project is always POS; the script runs one path unconditionally. - **Config key renames from parent** — `pos_printer_ip` → `printer_ip`, added `printer_port`. Removed: `printer_name`, `remote_path`, `telegram_token`, `telegram_chat_id`, `whatsapp_number`. - **`config.json` not committed** — `.gitignore` excludes it. Use `config.example.json` as template. -- **This machine is CT 166 (`.175`), not CT 629 (`.174`)** — CT 629 (OpenClaw) is stopped. Cron and manual runs both happen on CT 166 (OpenClaw2). Confirmed via `ip addr`. +- **CT 166 (openclaw2) and CT 629 (openclaw) both retired.** POS-Automation now runs on steel141. ### Bug fixes & discoveries - **Missing weather on cron run** — WMO weather code map was incomplete (e.g. code 73 = moderate snow not mapped, returned `'Unk'`). Expanded map to cover all common WMO codes (51-55 drizzle, 61-65 rain, 71-77 snow/sleet, 80-86 showers, 95-99 storms). Also increased API timeout from 5s → 15s to prevent silent failures. Bare `except: return None` was swallowing the error with no output. - **Telegram alerts firing unexpectedly** — parent script's top-level `except` block sent Telegram on any unhandled exception. When FreshRSS was unreachable, the no-items fallback crashed on `cups.Connection()` and triggered it. Fixed by removing CUPS and Telegram entirely. -- **FreshRSS unreachable from CT 166 via direct IP** — intermittent `Connection refused` on `192.168.0.157:80` even though host is pingable and Apache is running inside CT 120. Appears to be a transient Proxmox vmbr ARP/firewall state issue — resolves on its own. `fresh.sethpc.xyz` (Caddy) works as fallback but can return 502 when proxying slowly. Best approach: use direct IP, accept occasional failures. -- **Gitea direct IP (`192.168.0.125:80`) unreachable from CT 166** — must use `git.sethpc.xyz` (Caddy, port 443). +- **FreshRSS unreachable via direct IP** — intermittent `Connection refused` on `192.168.0.157:80` even though host is pingable and Apache is running inside CT 120. Appears to be a transient Proxmox vmbr ARP/firewall state issue — resolves on its own. `fresh.sethpc.xyz` (Caddy) works as fallback but can return 502 when proxying slowly. Best approach: use direct IP, accept occasional failures. +- **Gitea direct IP (`192.168.0.125:80`) unreachable** — must use `git.sethpc.xyz` (Caddy, port 443). - **Archive of old project** — original files from `printer-automation/` were moved to `printer-automation/Archive/` before the Gitea clone was pulled in. Old `summarize_news.py` is preserved there if needed. ### Open threads diff --git a/pos_briefing.py b/pos_briefing.py index 1d8c95b..662c6e6 100644 --- a/pos_briefing.py +++ b/pos_briefing.py @@ -10,6 +10,7 @@ import socket import requests import yfinance as yf from datetime import datetime +from zoneinfo import ZoneInfo # ── Config ──────────────────────────────────────────────────────────────────── CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") @@ -30,6 +31,7 @@ REMOTE_HOST = config["remote_host"] REMOTE_PASS = config["remote_pass"] PRINTER_IP = config["printer_ip"] PRINTER_PORT = int(config.get("printer_port", 9100)) +EASTERN_TZ = ZoneInfo("America/New_York") # TM-m30 80mm paper, Font B — column width measured from test print COLS = 57 @@ -246,11 +248,19 @@ def get_weather(): r = requests.get( "https://api.open-meteo.com/v1/forecast" "?latitude=38.8421&longitude=-77.2683" - "&daily=weathercode,temperature_2m_max,temperature_2m_min,precipitation_probability_max" - "&temperature_unit=fahrenheit&timezone=auto&forecast_days=4", + "&hourly=weathercode,temperature_2m,precipitation_probability" + "&temperature_unit=fahrenheit&timezone=America%2FNew_York&forecast_days=2", timeout=15 ).json() - daily = r.get("daily", {}) + hourly = r.get("hourly", {}) + times = hourly.get("time", []) + temps = hourly.get("temperature_2m", []) + pops = hourly.get("precipitation_probability", []) + codes = hourly.get("weathercode", []) + if not times: + return None + + now = datetime.now(EASTERN_TZ).replace(tzinfo=None) wmo = { 0: "Sun", 1: "Sun", 2: "PCld", 3: "Ovcst", 45: "Fog", 48: "Fog", @@ -261,15 +271,29 @@ def get_weather(): 85: "SnShwr", 86: "HvSnShwr", 95: "Strm", 96: "Strm", 99: "Strm", } + slots = [] + for i, t in enumerate(times): + dt = datetime.strptime(t, "%Y-%m-%dT%H:%M") + if dt >= now: + slots.append((dt, i)) + if len(slots) >= 24: + break + + if not slots: + return None + parts = [] - for i in range(len(daily.get("time", []))): - d = datetime.strptime(daily["time"][i], "%Y-%m-%d").strftime("%a") - cond = wmo.get(daily["weathercode"][i], "Unk") - hi = daily["temperature_2m_max"][i] - lo = daily["temperature_2m_min"][i] - pop = daily["precipitation_probability_max"][i] - parts.append(f"{d}:{cond} {hi:.0f}/{lo:.0f}({pop}%)") - return "|".join(parts) + for idx in range(0, len(slots), 3): + dt, i = slots[idx] + cond = wmo.get(codes[i], "Unk") + temp = temps[i] + pop = pops[i] + parts.append(f"{dt.strftime('%H:%M')}:{cond} {temp:.0f}F({pop:.0f}%)") + + lines = [] + for i in range(0, len(parts), 4): + lines.append(" | ".join(parts[i:i + 4])) + return lines except Exception as e: print(f"[!] Weather error: {e}") return None @@ -349,7 +373,7 @@ def build_receipt(articles, zfs, reddit, grafana, weather, finance): s(align='center', bold=True) p.text("Daily Briefing\n") s(align='center', bold=False) - p.text(datetime.now().strftime('%A, %B %d %I:%M %p') + "\n") + p.text(datetime.now(EASTERN_TZ).strftime('%A, %B %d %H:%M ET') + "\n") p.text("=" * COLS + "\n") # Dashboard @@ -357,7 +381,12 @@ def build_receipt(articles, zfs, reddit, grafana, weather, finance): p.text("SYSTEM & WEATHER\n") s(align='left', bold=False) if weather: - p.text(f"Wx: {weather}\n") + if isinstance(weather, list): + p.text("Wx24:\n") + for line in weather: + p.text(f" {line}\n") + else: + p.text(f"Wx: {weather}\n") if zfs: p.text(f"ZFS: {zfs['alloc']}/{zfs['size']} ({zfs['cap']}) {zfs['health']}\n") if grafana: