From 924366ac6c9fea9832c46677f4bcdd2d61067707 Mon Sep 17 00:00:00 2001 From: Stefan Lohmaier Date: Fri, 12 Jun 2026 07:28:00 +0200 Subject: [PATCH] Add travel time, geocoding, and reminders to create_event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- calendar/server.py | 51 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/calendar/server.py b/calendar/server.py index e428b8f..2168c25 100644 --- a/calendar/server.py +++ b/calendar/server.py @@ -185,17 +185,33 @@ def get_tasks( 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() def create_event( calendar: Annotated[str, Field(description="Calendar name, e.g. 'Stefan', 'Arbeit', 'Familie'")], 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'")], 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")] = "", 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: - """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() if not user: return "Error: not authenticated" cals = [c for c in _discover(user, "VEVENT") if calendar.lower() in c["name"].lower()] @@ -211,11 +227,36 @@ def create_event( else: ev.add("dtstart").value = datetime.fromisoformat(start) ev.add("dtend").value = datetime.fromisoformat(end) - if location: ev.add("location").value = location - if description: ev.add("description").value = description + if location: + 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() 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()