feat: hourly weather forecast + steel141 migration notes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+3
-3
@@ -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
|
||||
|
||||
+9
-3
@@ -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
|
||||
|
||||
+41
-12
@@ -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,6 +381,11 @@ def build_receipt(articles, zfs, reddit, grafana, weather, finance):
|
||||
p.text("SYSTEM & WEATHER\n")
|
||||
s(align='left', bold=False)
|
||||
if weather:
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user