Add Home Assistant calendar sync via webhook
All checks were successful
Deploy / deploy (push) Successful in 42s

Every successful menu fetch now POSTs the day's items to HA_WEBHOOK_URL
(when set), so an HA automation can turn it into a calendar event. Surface
the push result as a "Calendar Sync" badge on the status card, hidden
unless HA_WEBHOOK_URL is configured.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KsJqAZ82Veyf5oihBr71S5
This commit is contained in:
Barely Removable 2026-09-03 07:05:31 -07:00
parent 83533d339d
commit a9ea0a2d87
3 changed files with 45 additions and 3 deletions

38
app.py
View file

@ -12,6 +12,7 @@ from datetime import date, datetime
from email.message import EmailMessage
from pathlib import Path
import requests
import schedule
from flask import Flask, jsonify, request
@ -34,7 +35,7 @@ _DEFAULTS = {
"vacations": [],
}
_status = {"last_run": None, "last_result": None, "last_message": None}
_status = {"last_run": None, "last_result": None, "last_message": None, "calendar_result": None}
_lock = threading.Lock()
@ -82,6 +83,18 @@ def _send_email_sms(message: str, phone: str, gateway: str) -> None:
log.info(f"Sent ✓ → {to_addr}")
def _push_to_home_assistant(items: list[str], date_str: str) -> bool:
"""Returns True if a push was attempted and succeeded, False if skipped (not configured)."""
webhook_url = os.environ.get("HA_WEBHOOK_URL")
if not webhook_url:
return False
log.info(f"Pushing menu → Home Assistant webhook")
resp = requests.post(webhook_url, json={"date": date_str, "items": items}, timeout=10)
resp.raise_for_status()
log.info("Home Assistant push ✓")
return True
def run_job() -> None:
global _status
settings = load_settings()
@ -104,6 +117,7 @@ def run_job() -> None:
message = f"No lunch menu posted for {date_str} (may be a break or holiday)."
except Exception as e:
log.exception("Menu fetch failed")
items, date_str = [], None
message = f"Error fetching Mariposa lunch menu: {e}"
if len(message) > 1400:
@ -111,6 +125,14 @@ def run_job() -> None:
log.info(f"Message:\n{message}")
calendar_result = "n/a"
if items:
try:
calendar_result = "ok" if _push_to_home_assistant(items, date_str) else "n/a"
except Exception as e:
log.error(f"Home Assistant push failed: {e}")
calendar_result = f"error: {e}"
errors = []
for phone in phones:
try:
@ -123,6 +145,7 @@ def run_job() -> None:
"last_run": datetime.now().isoformat(),
"last_result": "error: " + "; ".join(errors) if errors else "ok",
"last_message": message,
"calendar_result": calendar_result,
}
@ -168,8 +191,7 @@ def index():
@app.route("/icon.png")
def icon():
import requests as req
r = req.get(MASCOT_URL, timeout=10)
r = requests.get(MASCOT_URL, timeout=10)
return r.content, 200, {"Content-Type": "image/png", "Cache-Control": "public, max-age=86400"}
@ -346,6 +368,7 @@ input:focus { outline: 2px solid var(--primary); outline-offset: -1px; border-co
<div class="stat"><dt>Result</dt><dd id="s-result">—</dd></div>
<div class="stat"><dt>Next Scheduled</dt><dd id="s-next">—</dd></div>
<div class="stat"><dt>Mode</dt><dd id="s-mode">—</dd></div>
<div class="stat" id="s-cal-stat" style="display:none;"><dt>Calendar Sync</dt><dd id="s-cal">—</dd></div>
</dl>
<div id="s-last-msg" style="display:none;margin-top:14px;padding:10px 12px;background:#1c2128;border-radius:8px;font-size:.8rem;color:#c9d1d9;white-space:pre-wrap;"></div>
</div>
@ -479,6 +502,15 @@ function applyStatus(st) {
document.getElementById('s-mode').innerHTML = vac
? `<span class="badge badge-yellow">Vacation: ${vac}</span>`
: '<span class="badge badge-green">Active</span>';
const calStat = document.getElementById('s-cal-stat');
const cal = st.calendar_result;
if (cal && cal !== 'n/a') {
calStat.style.display = '';
const calCls = cal === 'ok' ? 'badge-green' : 'badge-red';
document.getElementById('s-cal').innerHTML = `<span class="badge ${calCls}">${cal}</span>`;
} else {
calStat.style.display = 'none';
}
const msgEl = document.getElementById('s-last-msg');
if (st.last_message) {
msgEl.textContent = st.last_message;