feat: hourly weather forecast + steel141 migration notes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mortdecai
2026-06-07 13:15:02 -04:00
parent c7b114da7a
commit f19c04bdc1
3 changed files with 54 additions and 19 deletions
+3 -3
View File
@@ -45,10 +45,10 @@ Forked from `Seth/PrinterAutomation` on Gitea.
- **Grafana:** `monitor.atermiter-x79.xyz` — Proxmox node metrics (173, 112, 197). - **Grafana:** `monitor.atermiter-x79.xyz` — Proxmox node metrics (173, 112, 197).
- **ZFS:** SSH to `192.168.0.197` (node-197) — queries `zpool list tank`. - **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). - **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 ## 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 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 ## Known Issues / Notes
- `pct exec 147` (Uptime Kuma check) only works on a Proxmox host, not inside a CT — silently returns None. - `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. - `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. - yfinance ETH-USD occasionally fails with a NoneType error — non-fatal, other tickers still print.
## Gitea ## Gitea
+9 -3
View File
@@ -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. 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 ## Gitea
- Repo: `https://git.sethpc.xyz/Seth/POS-Automation` - 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. - **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 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. - **`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 ### 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. - **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. - **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. - **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 from CT 166** — must use `git.sethpc.xyz` (Caddy, port 443). - **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. - **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 ### Open threads
+42 -13
View File
@@ -10,6 +10,7 @@ import socket
import requests import requests
import yfinance as yf import yfinance as yf
from datetime import datetime from datetime import datetime
from zoneinfo import ZoneInfo
# ── Config ──────────────────────────────────────────────────────────────────── # ── Config ────────────────────────────────────────────────────────────────────
CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") 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"] REMOTE_PASS = config["remote_pass"]
PRINTER_IP = config["printer_ip"] PRINTER_IP = config["printer_ip"]
PRINTER_PORT = int(config.get("printer_port", 9100)) 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 # TM-m30 80mm paper, Font B — column width measured from test print
COLS = 57 COLS = 57
@@ -246,11 +248,19 @@ def get_weather():
r = requests.get( r = requests.get(
"https://api.open-meteo.com/v1/forecast" "https://api.open-meteo.com/v1/forecast"
"?latitude=38.8421&longitude=-77.2683" "?latitude=38.8421&longitude=-77.2683"
"&daily=weathercode,temperature_2m_max,temperature_2m_min,precipitation_probability_max" "&hourly=weathercode,temperature_2m,precipitation_probability"
"&temperature_unit=fahrenheit&timezone=auto&forecast_days=4", "&temperature_unit=fahrenheit&timezone=America%2FNew_York&forecast_days=2",
timeout=15 timeout=15
).json() ).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 = { wmo = {
0: "Sun", 1: "Sun", 2: "PCld", 3: "Ovcst", 0: "Sun", 1: "Sun", 2: "PCld", 3: "Ovcst",
45: "Fog", 48: "Fog", 45: "Fog", 48: "Fog",
@@ -261,15 +271,29 @@ def get_weather():
85: "SnShwr", 86: "HvSnShwr", 85: "SnShwr", 86: "HvSnShwr",
95: "Strm", 96: "Strm", 99: "Strm", 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 = [] parts = []
for i in range(len(daily.get("time", []))): for idx in range(0, len(slots), 3):
d = datetime.strptime(daily["time"][i], "%Y-%m-%d").strftime("%a") dt, i = slots[idx]
cond = wmo.get(daily["weathercode"][i], "Unk") cond = wmo.get(codes[i], "Unk")
hi = daily["temperature_2m_max"][i] temp = temps[i]
lo = daily["temperature_2m_min"][i] pop = pops[i]
pop = daily["precipitation_probability_max"][i] parts.append(f"{dt.strftime('%H:%M')}:{cond} {temp:.0f}F({pop:.0f}%)")
parts.append(f"{d}:{cond} {hi:.0f}/{lo:.0f}({pop}%)")
return "|".join(parts) lines = []
for i in range(0, len(parts), 4):
lines.append(" | ".join(parts[i:i + 4]))
return lines
except Exception as e: except Exception as e:
print(f"[!] Weather error: {e}") print(f"[!] Weather error: {e}")
return None return None
@@ -349,7 +373,7 @@ def build_receipt(articles, zfs, reddit, grafana, weather, finance):
s(align='center', bold=True) s(align='center', bold=True)
p.text("Daily Briefing\n") p.text("Daily Briefing\n")
s(align='center', bold=False) 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") p.text("=" * COLS + "\n")
# Dashboard # Dashboard
@@ -357,7 +381,12 @@ def build_receipt(articles, zfs, reddit, grafana, weather, finance):
p.text("SYSTEM & WEATHER\n") p.text("SYSTEM & WEATHER\n")
s(align='left', bold=False) s(align='left', bold=False)
if weather: 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: if zfs:
p.text(f"ZFS: {zfs['alloc']}/{zfs['size']} ({zfs['cap']}) {zfs['health']}\n") p.text(f"ZFS: {zfs['alloc']}/{zfs['size']} ({zfs['cap']}) {zfs['health']}\n")
if grafana: if grafana: