📄 today.py 7,616 bytes Apr 28, 2026 📋 Raw

"""Today API — Aggregate calendar, weather, and health for dashboard."""
import os
import json
import httpx
import asyncio
from datetime import datetime, timezone
from typing import Optional

Radicale CalDAV config

RADICALE_URL = os.getenv("RADICALE_URL", "http://localhost:5232")
RADICALE_USER = os.getenv("RADICALE_USER", "assistant")
RADICALE_PASS = os.getenv("RADICALE_PASS", "family-assistant-2026")

Weather config

WEATHER_LOCATION = os.getenv("WEATHER_LOCATION", "Green Bay, WI")

async def fetch_calendar_today() -> dict:
"""Fetch today's events from Radicale CalDAV."""
try:
import caldav

    client = caldav.DAVClient(
        url=f"{RADICALE_URL}/",
        username=RADICALE_USER,
        password=RADICALE_PASS
    )

    principal = client.principal()
    calendars = principal.calendars()

    if not calendars:
        return {"status": "error", "error": "No calendars found"}

    # Get events for today
    today = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
    tomorrow = today.replace(day=today.day + 1)

    events = []
    for cal in calendars:
        try:
            cal_events = cal.date_search(start=today, end=tomorrow)
            for event in cal_events:
                vevent = event.instance.vobject_instance.vevent
                events.append({
                    "summary": str(vevent.summary.value) if hasattr(vevent, 'summary') else "Untitled",
                    "start": vevent.dtstart.value.isoformat() if hasattr(vevent, 'dtstart') else today.isoformat(),
                    "end": vevent.dtend.value.isoformat() if hasattr(vevent, 'dtend') else None,
                    "location": str(vevent.location.value) if hasattr(vevent, 'location') else None,
                    "is_all_day": not hasattr(vevent.dtstart.value, 'hour')
                })
        except Exception:
            continue

    # Sort by start time
    events.sort(key=lambda x: x["start"])

    return {"status": "ok", "events": events}

except ImportError:
    return {"status": "error", "error": "caldav not installed"}
except Exception as e:
    return {"status": "error", "error": str(e)}

async def fetch_weather() -> dict:
"""Fetch weather from wttr.in (no API key)."""
try:
location = WEATHER_LOCATION.replace(" ", "+")
async with httpx.AsyncClient() as client:
response = await client.get(
f"https://wttr.in/{location}?format=j1",
timeout=10.0
)
data = response.json()

        current = data.get("current_condition", [{}])[0]

        # Get next 6 hours from hourly forecast
        hourly = data.get("weather", [{}])[0].get("hourly", [])[:6]
        forecast = []
        for hour in hourly:
            # Fix hour formatting (wttr.in returns 0, 300, 600, etc.)
            time_raw = hour.get("time", "0")
            if len(time_raw) >= 3:
                hour_str = time_raw[:-2] if len(time_raw) > 2 else "0"
            else:
                hour_str = time_raw
            try:
                hour_int = int(hour_str)
                hour_display = f"{hour_int % 24}:00"
            except ValueError:
                hour_display = time_raw

            forecast.append({
                "hour": hour_display,
                "temperature_f": float(hour.get("tempF", 0)),
                "precipitation_chance_pct": int(hour.get("chanceofrain", 0))
            })

        return {
            "status": "ok",
            "current": {
                "temperature_f": float(current.get("temp_F", 0)),
                "feels_like_f": float(current.get("FeelsLikeF", 0)),
                "condition": current.get("weatherDesc", [{"value": "Unknown"}])[0].get("value", "Unknown"),
                "humidity_pct": int(current.get("humidity", 0)),
                "wind_mph": float(current.get("windspeedMiles", 0))
            },
            "forecast": forecast
        }

except Exception as e:
    return {"status": "error", "error": str(e)}

async def fetch_health() -> dict:
"""Ping system components and return health status."""
components = {}

# OpenClaw Gateway
try:
    async with httpx.AsyncClient() as client:
        r = await client.get("http://localhost:18789/health", timeout=5.0)
        components["openclaw_gateway"] = {
            "status": "ok" if r.status_code == 200 else "degraded",
            "latency_ms": int(r.elapsed.total_seconds() * 1000)
        }
except Exception:
    components["openclaw_gateway"] = {"status": "critical", "latency_ms": 9999}

# Radicale CalDAV
try:
    async with httpx.AsyncClient() as client:
        r = await client.get(f"{RADICALE_URL}/.well-known/caldav", timeout=5.0)
        components["radicale_caldav"] = {
            "status": "ok" if r.status_code in [200, 301, 308] else "degraded",
            "latency_ms": int(r.elapsed.total_seconds() * 1000)
        }
except Exception:
    components["radicale_caldav"] = {"status": "critical", "latency_ms": 9999}

# Ollama Local
try:
    async with httpx.AsyncClient() as client:
        r = await client.get("http://localhost:11434/api/tags", timeout=5.0)
        components["ollama_local"] = {
            "status": "ok" if r.status_code == 200 else "degraded",
            "latency_ms": int(r.elapsed.total_seconds() * 1000)
        }
except Exception:
    components["ollama_local"] = {"status": "critical", "latency_ms": 9999}

# Cloudflare Tunnel
try:
    async with httpx.AsyncClient() as client:
        r = await client.get("https://hoffdesk.com/health", timeout=10.0)
        components["cloudflare_tunnel"] = {
            "status": "ok" if r.status_code == 200 else "degraded",
            "latency_ms": int(r.elapsed.total_seconds() * 1000)
        }
except Exception:
    components["cloudflare_tunnel"] = {"status": "critical", "latency_ms": 9999}

# Disk usage
try:
    import shutil
    total, used, free = shutil.disk_usage("/")
    disk_usage = {
        "total_gb": round(total / (1024**3), 1),
        "used_gb": round(used / (1024**3), 1),
        "usage_pct": round((used / total) * 100, 1)
    }
except Exception:
    disk_usage = {"total_gb": 0, "used_gb": 0, "usage_pct": 0}

# Overall status
statuses = [c["status"] for c in components.values()]
if any(s == "critical" for s in statuses):
    overall = "critical"
elif any(s == "degraded" for s in statuses):
    overall = "degraded"
else:
    overall = "healthy"

return {
    "overall_status": overall,
    "components": components,
    "disk_usage": disk_usage
}

async def get_today_data() -> dict:
"""Aggregate all today data."""
calendar, weather, health = await asyncio.gather(
fetch_calendar_today(),
fetch_weather(),
fetch_health()
)

return {
    "calendar": calendar,
    "weather": weather,
    "health": health,
    "generated_at": datetime.now(timezone.utc).isoformat()
}