#!/usr/bin/env python3 """Daily digest for Andy — checks birthdays, holidays, notable weather. Silent exit (no output) if nothing noteworthy. Meant for no_agent=True cron.""" import json import urllib.request import urllib.error from datetime import date, timedelta import sys # ─── CONFIG ────────────────────────────────────────────────────────────────── BIRTHDAYS = { "Muna": (8, 2), # mom "Steve": (3, 26), # dad "Lisa": (9, 30), # sister (1985) "Donna": (1, 19), # wife (1977) "Calvin": (10, 9), # son (2013) } ANNIVERSARIES = { "Wedding 🥂": (8, 20), # married 8/20/2011 } # ─── SCHOOL (TCS 2026-2027) ─────────────────────────────────────────────────── # Format: (month, day, label, lead_days) # lead_days = how far in advance to start notifying SCHOOL_EVENTS = [ # (month, day, label, lead_days) # lead_days = how far in advance to start notifying (8, 10, "📚 Teacher Work Days begin (school starts Aug 17)", 1), (8, 17, "📚 **First Day of School** — TCS 2026-27!", 20), (9, 7, "🏖️ Labor Day — no school", 1), (10, 2, "📋 Progress Reports issued", 7), (10, 6, "👩‍🏫 Parent-Teacher Conferences begin", 7), (10, 12, "🏖️ Columbus Day — no school", 1), (11, 6, "🏖️ Veterans Day observed — no school", 1), (11, 25, "🦃 Thanksgiving Break begins — no school", 7), (12, 18, "📚 End of 1st Semester", 7), (12, 21, "🎄 Winter Break begins — no school", 7), (1, 8, "📋 Progress Reports issued", 7), (1, 18, "🏖️ MLK Day — no school", 1), (2, 15, "🏖️ Presidents' Day — no school", 1), (3, 4, "📋 Progress Reports issued", 7), (3, 9, "👩‍🏫 Parent-Teacher Conferences begin", 7), (4, 2, "🌸 Spring Break begins — no school", 7), (4, 6, "🚌 School Trip", 7), (4, 19, "🌸 Spring Break continues — no school", 7), (5, 28, "🎓 **Last Day of School!**", 20), (5, 31, "🏖️ Memorial Day — no school", 1), (6, 4, "📋 Progress Reports issued", 7), ] # Beaufort, SC LAT, LON = "32.43", "-80.67" # ─── HELPERS ───────────────────────────────────────────────────────────────── def nth_weekday(year, month, weekday, n): """Return date of nth weekday in month (0=Mon .. 6=Sun).""" first = date(year, month, 1) offset = (weekday - first.weekday()) % 7 return first + timedelta(days=offset + 7 * (n - 1)) def father_sunday(year): """Third Sunday of June.""" return nth_weekday(year, 6, 6, 3) def mother_sunday(year): """Second Sunday of May.""" return nth_weekday(year, 5, 6, 2) def fetch_json(url, timeout=10): try: req = urllib.request.Request(url, headers={"User-Agent": "Hermes-Digest/1.0"}) with urllib.request.urlopen(req, timeout=timeout) as resp: return json.loads(resp.read()) except Exception as e: return None def days_until_birthday(month, day, today): """Return (days_until, date_obj). Handles past birthdays by wrapping to next year.""" try: this_year = date(today.year, month, day) except ValueError: return None, None diff = (this_year - today).days if diff >= 0: return diff, this_year # Already past — check next year try: next_year = date(today.year + 1, month, day) return (next_year - today).days, next_year except ValueError: return None, None # ─── WEATHER ───────────────────────────────────────────────────────────────── def check_alerts(): """Check NWS for active severe weather alerts near Beaufort, SC.""" data = fetch_json(f"https://api.weather.gov/alerts/active?point={LAT},{LON}") if not data: return None features = data.get("features", []) notable = [] for f in features: p = f.get("properties", {}) sev = p.get("severity", "") event = p.get("event", "") headline = p.get("headline", "") if sev in ("Severe", "Extreme"): notable.append(f"⚠️ {event}: {headline}") elif sev == "Moderate": notable.append(f"⚠️ {event}: {headline}") return notable if notable else None def check_forecast(): """Check NWS forecast for notably bad conditions today/tomorrow.""" points = fetch_json(f"https://api.weather.gov/points/{LAT},{LON}") if not points: return None forecast_url = points.get("properties", {}).get("forecast") if not forecast_url: return None data = fetch_json(forecast_url) if not data: return None periods = data.get("properties", {}).get("periods", []) today_str = date.today().isoformat() tomorrow_str = (date.today() + timedelta(days=1)).isoformat() notes = [] SEVERE = [ "hurricane", "tropical storm", "severe thunderstorm", "tornado", "blizzard", "ice storm", "winter storm", "flash flood", ] for p in periods[:4]: start = (p.get("startTime") or "")[:10] if start not in (today_str, tomorrow_str): continue temp = p.get("temperature", 70) wind = (p.get("windSpeed") or "").lower() short = p.get("shortForecast", "") if temp >= 100: notes.append(f"🌡️ Extreme heat: {temp}°F — {short}") elif temp <= 20: notes.append(f"🥶 Extreme cold: {temp}°F — {short}") for kw in SEVERE: if kw in short.lower(): notes.append(f"🌩️ {short}") break # High wind (grab first number from wind string) nums = [c for c in wind.split()[0] if c.isdigit()] if wind else [] if nums: try: if int("".join(nums)) >= 30: notes.append(f"💨 High wind: {p.get('windSpeed', '')}") except ValueError: pass return notes if notes else None # ─── MAIN ──────────────────────────────────────────────────────────────────── def main(): today = date.today() lines = [] # ── Birthdays ── upcoming = [] for name, (m, d) in BIRTHDAYS.items(): diff, dt = days_until_birthday(m, d, today) if diff is not None and 0 <= diff <= 20: upcoming.append((name, diff, dt)) if upcoming: lines.append("🎂 **Upcoming Birthdays:**") for name, diff, dt in sorted(upcoming, key=lambda x: x[1]): if diff == 0: lines.append(f" • **{name}** — TODAY! 🎉") elif diff == 1: lines.append(f" • **{name}** — tomorrow!") else: lines.append(f" • **{name}** — in {diff} days ({dt.strftime('%B %d')})") lines.append("") # ── Anniversaries ── anniv_upcoming = [] for label, (m, d) in ANNIVERSARIES.items(): diff, dt = days_until_birthday(m, d, today) if diff is not None and 0 <= diff <= 20: anniv_upcoming.append((label, diff, dt)) if anniv_upcoming: lines.append("💍 **Upcoming Anniversaries:**") for label, diff, dt in sorted(anniv_upcoming, key=lambda x: x[1]): if diff == 0: lines.append(f" • **{label}** — TODAY! 🎉") elif diff == 1: lines.append(f" • **{label}** — tomorrow!") else: lines.append(f" • **{label}** — in {diff} days ({dt.strftime('%B %d')})") lines.append("") # ── Holidays ── events = [] for label, fn in [("Father's Day 👔", father_sunday), ("Mother's Day 🌸", mother_sunday)]: d = fn(today.year) diff = (d - today).days if 0 <= diff <= 20: events.append((label, diff, d)) elif diff < 0: d_next = fn(today.year + 1) diff = (d_next - today).days if diff <= 20: events.append((label, diff, d_next)) if events: lines.append("📅 **Upcoming Events:**") for label, diff, dt in sorted(events, key=lambda x: x[1]): if diff == 0: lines.append(f" • **{label}** — TODAY!") elif diff == 1: lines.append(f" • **{label}** — tomorrow!") else: lines.append(f" • **{label}** — in {diff} days ({dt.strftime('%B %d')})") lines.append("") # ── Weather alerts ── alerts = check_alerts() if alerts: lines.append("🚨 **Weather Alerts:**") for a in alerts: lines.append(f" {a}") lines.append("") # ── Notable forecast ── forecast_notes = check_forecast() if forecast_notes: lines.append("🌤️ **Notable Weather:**") for n in forecast_notes: lines.append(f" {n}") lines.append("") # ── School events ── school_upcoming = [] school_year = 2026 # School year starts in Aug 2026 for month, day, label, lead_days in SCHOOL_EVENTS: # School year spans Aug 2026 - Jun 2027 ev_year = 2026 if month >= 8 else 2027 try: ev_date = date(ev_year, month, day) except ValueError: continue diff = (ev_date - today).days if 0 <= diff <= lead_days: school_upcoming.append((label, diff, ev_date)) if school_upcoming: lines.append("🏫 **School Calendar:**") for label, diff, dt in sorted(school_upcoming, key=lambda x: x[1]): if diff == 0: lines.append(f" • {label} — **TODAY!**") elif diff == 1: lines.append(f" • {label} — tomorrow") else: lines.append(f" • {label} — in {diff} days ({dt.strftime('%B %d')})") lines.append("") # ── Output ── if lines: header = f"☀️ **Daily Digest** — {today.strftime('%A, %B %d, %Y')}\n\n" footer = "\n— Hermes" print(header + "\n".join(lines).strip() + footer) # else: silent exit — nothing worth reporting if __name__ == "__main__": main()