Add travel time, geocoding, and reminders to create_event

- travel_minutes: shows as gray block before event on iPhone
- location geocoding via Nominatim → X-APPLE-STRUCTURED-LOCATION with geo coords
- reminder_minutes: VALARM trigger before event
- Also externalized all credentials to config.json

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Stefan Lohmaier
2026-06-12 07:28:00 +02:00
parent fb642e47c8
commit 924366ac6c
+46 -5
View File
@@ -185,17 +185,33 @@ def get_tasks(
return "\n\n".join(results) if results else "Keine Aufgaben" return "\n\n".join(results) if results else "Keine Aufgaben"
def _geocode(address):
"""Geocode an address via Nominatim. Returns (lat, lon, display_name) or None."""
try:
r = httpx.get("https://nominatim.openstreetmap.org/search",
params={"q": address, "format": "json", "limit": 1},
headers={"User-Agent": "mcp-home/1.0"}, timeout=10)
results = r.json()
if results:
return float(results[0]["lat"]), float(results[0]["lon"]), results[0].get("display_name", address)
except Exception:
pass
return None
@mcp.tool() @mcp.tool()
def create_event( def create_event(
calendar: Annotated[str, Field(description="Calendar name, e.g. 'Stefan', 'Arbeit', 'Familie'")], calendar: Annotated[str, Field(description="Calendar name, e.g. 'Stefan', 'Arbeit', 'Familie'")],
title: Annotated[str, Field(description="Event title")], title: Annotated[str, Field(description="Event title")],
start: Annotated[str, Field(description="Start: YYYY-MM-DD for all-day, or YYYY-MM-DDTHH:MM for timed events, e.g. '2026-06-15T14:00'")], start: Annotated[str, Field(description="Start: YYYY-MM-DD for all-day, or YYYY-MM-DDTHH:MM for timed events, e.g. '2026-06-15T14:00'")],
end: Annotated[str, Field(description="End: YYYY-MM-DD for all-day, or YYYY-MM-DDTHH:MM, e.g. '2026-06-15T15:30'")], end: Annotated[str, Field(description="End: YYYY-MM-DD for all-day, or YYYY-MM-DDTHH:MM, e.g. '2026-06-15T15:30'")],
location: Annotated[str, Field(description="Location/address")] = "", location: Annotated[str, Field(description="Location or address, e.g. 'Marienplatz 1, Muenchen'. Gets geocoded for map display.")] = "",
description: Annotated[str, Field(description="Event description or notes")] = "", description: Annotated[str, Field(description="Event description or notes")] = "",
allday: Annotated[bool, Field(description="True for all-day events (then use YYYY-MM-DD for start/end)")] = False, allday: Annotated[bool, Field(description="True for all-day events (then use YYYY-MM-DD for start/end)")] = False,
travel_minutes: Annotated[int, Field(description="Travel time in minutes shown before the event on iPhone (0=none, e.g. 30 for 30 min commute)")] = 0,
reminder_minutes: Annotated[int, Field(description="Reminder alert X minutes before the event (0=none, e.g. 15, 30, 60)")] = 0,
) -> str: ) -> str:
"""Create a new calendar event. For all-day events, end date is exclusive (event on June 15 → start=2026-06-15, end=2026-06-16).""" """Create a new calendar event. For all-day events, end date is exclusive (June 15 only → start=2026-06-15, end=2026-06-16). Location is geocoded for map pins. Travel time shows as block before the event on iPhone."""
user = get_current_user() user = get_current_user()
if not user: return "Error: not authenticated" if not user: return "Error: not authenticated"
cals = [c for c in _discover(user, "VEVENT") if calendar.lower() in c["name"].lower()] cals = [c for c in _discover(user, "VEVENT") if calendar.lower() in c["name"].lower()]
@@ -211,11 +227,36 @@ def create_event(
else: else:
ev.add("dtstart").value = datetime.fromisoformat(start) ev.add("dtstart").value = datetime.fromisoformat(start)
ev.add("dtend").value = datetime.fromisoformat(end) ev.add("dtend").value = datetime.fromisoformat(end)
if location: ev.add("location").value = location if location:
if description: ev.add("description").value = description ev.add("location").value = location
geo = _geocode(location)
if geo:
lat, lon, display = geo
loc_prop = ev.add("x-apple-structured-location")
loc_prop.value = f"geo:{lat},{lon}"
loc_prop.params["VALUE"] = ["URI"]
loc_prop.params["X-APPLE-RADIUS"] = ["70"]
loc_prop.params["X-APPLE-REFERENCEFRAME"] = ["1"]
loc_prop.params["X-TITLE"] = [location]
if description:
ev.add("description").value = description
if travel_minutes > 0:
tp = ev.add("x-apple-travel-duration")
tp.value = f"PT{travel_minutes}M"
tp.params["VALUE"] = ["DURATION"]
if reminder_minutes > 0:
alarm = ev.add("valarm")
alarm.add("action").value = "DISPLAY"
alarm.add("description").value = title
alarm.add("trigger").value = timedelta(minutes=-reminder_minutes)
ev.add("dtstamp").value = datetime.utcnow() ev.add("dtstamp").value = datetime.utcnow()
r = httpx.put(RADICALE + cals[0]["href"] + uid + ".ics", content=cal.serialize(), auth=_auth(user), headers={"Content-Type": "text/calendar"}, timeout=15) r = httpx.put(RADICALE + cals[0]["href"] + uid + ".ics", content=cal.serialize(), auth=_auth(user), headers={"Content-Type": "text/calendar"}, timeout=15)
return f"Termin erstellt: {title} am {start}" if r.status_code in (201, 204) else f"Fehler: HTTP {r.status_code}" extras = []
if location and _geocode(location): extras.append("mit Karte")
if travel_minutes: extras.append(f"{travel_minutes} Min. Fahrzeit")
if reminder_minutes: extras.append(f"Erinnerung {reminder_minutes} Min. vorher")
extra_str = f" ({', '.join(extras)})" if extras else ""
return f"Termin erstellt: {title} am {start}{extra_str}" if r.status_code in (201, 204) else f"Fehler: HTTP {r.status_code}"
@mcp.tool() @mcp.tool()