From 7047f55cf70a2642b677f19dfc868d040174083e Mon Sep 17 00:00:00 2001 From: Guido Schmit Date: Wed, 29 Apr 2026 19:07:02 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20HA-Termine=20erstellen=20=C3=BCber=20ca?= =?UTF-8?q?lendar.create=5Fevent=20Service=20-=20POST=20/api/homeassistant?= =?UTF-8?q?/events=20Endpoint=20mit=20calendar.create=5Fevent=20-=20Fronte?= =?UTF-8?q?nd:=20HA-Termine=20erstellen=20statt=20'nicht=20unterst=C3=BCtz?= =?UTF-8?q?t'=20Toast=20-=20Datetime-Format=20an=20HA-Konvention=20angepas?= =?UTF-8?q?st:=20=20=20'YYYY-MM-DD=20HH:MM:SS'=20(Space-Separator,=20ohne?= =?UTF-8?q?=20Timezone)=20-=20=5Fha=5Fformat=5Fdt=20Helper=20f=C3=BCr=20IS?= =?UTF-8?q?O=20=E2=86=92=20HA=20Datetime-Konvertierung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routers/homeassistant_router.py | 82 +++++++++++++++++++++++-- frontend/js/calendar.js | 8 ++- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/backend/routers/homeassistant_router.py b/backend/routers/homeassistant_router.py index faadd7e..8cfbede 100644 --- a/backend/routers/homeassistant_router.py +++ b/backend/routers/homeassistant_router.py @@ -109,6 +109,25 @@ def _ha_get_events(url: str, token: str, entity_id: str, start_dt: datetime, end raise http_requests.exceptions.Timeout(f"HA Timeout für {entity_id}") +def _ha_format_dt(s: str) -> str: + """Convert ISO datetime to HA format: 'YYYY-MM-DD HH:MM:SS' (no timezone).""" + # Strip timezone if present + if s.endswith("Z"): + s = s[:-1] + # Strip +HH:MM or -HH:MM offset + for sep in ("+", "-"): + idx = s.rfind(sep) + if idx > 10: + s = s[:idx] + break + # Replace T with space + s = s.replace("T", " ") + # Ensure seconds are present + if len(s) == 16: # 'YYYY-MM-DD HH:MM' + s += ":00" + return s + + def _ha_build_event_body(entity_id: str, data: dict) -> dict: """Build a service-call body for create_event / update_event.""" body = {"entity_id": entity_id} @@ -123,13 +142,29 @@ def _ha_build_event_body(entity_id: str, data: dict) -> dict: body["start_date"] = data["start"][:10] body["end_date"] = data["end"][:10] else: - s = data["start"].replace("Z", "+00:00") if data["start"].endswith("Z") else data["start"] - e = data["end"].replace("Z", "+00:00") if data["end"].endswith("Z") else data["end"] - body["start_date_time"] = s - body["end_date_time"] = e + body["start_date_time"] = _ha_format_dt(data["start"]) + body["end_date_time"] = _ha_format_dt(data["end"]) return body +def _ha_create_event(url: str, token: str, entity_id: str, data: dict) -> dict: + """Create a new event via HA calendar.create_event service.""" + base = url.rstrip("/") + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + body = _ha_build_event_body(entity_id, data) + resp = http_requests.post( + f"{base}/api/services/calendar/create_event", + headers=headers, json=body, timeout=15, verify=False, + ) + if not resp.ok: + try: + detail = resp.json().get("message", resp.text[:500]) + except Exception: + detail = resp.text[:500] if resp.text else f"HTTP {resp.status_code}" + raise Exception(f"HA create_event ({resp.status_code}): {detail}") + return resp + + def _ha_update_event(url: str, token: str, entity_id: str, uid: str, data: dict): """Update via update_event service, fallback to delete+create.""" base = url.rstrip("/") @@ -534,6 +569,45 @@ class HAEventUpdate(BaseModel): description: Optional[str] = None +class HAEventCreate(BaseModel): + calendar_id: int + title: str + start: str + end: str + allDay: bool = False + location: Optional[str] = None + description: Optional[str] = None + + +@router.post("/events") +def create_event( + data: HAEventCreate, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user), +): + cal = ( + db.query(models.HomeAssistantCalendar) + .join(models.HomeAssistantAccount) + .filter( + models.HomeAssistantCalendar.id == data.calendar_id, + models.HomeAssistantAccount.user_id == current_user.id, + ) + .first() + ) + if not cal: + raise HTTPException(404, "Calendar not found") + account = cal.account + token = _get_valid_token(account, db) + try: + _ha_create_event( + account.url, token, cal.entity_id, + data.model_dump(exclude_none=True), + ) + return {"ok": True} + except Exception as exc: + raise HTTPException(500, f"HA event create failed: {exc}") + + @router.put("/events/{calendar_id}/{uid}") def update_event( calendar_id: int, diff --git a/frontend/js/calendar.js b/frontend/js/calendar.js index dbf1bba..362af1c 100644 --- a/frontend/js/calendar.js +++ b/frontend/js/calendar.js @@ -1460,8 +1460,12 @@ function bindEventModal() { }); showToast(t('event_created')); } else if (isHA) { - showToast(t('ha_create_not_supported'), true); - return; + const haCalId = parseInt(calVal.replace('homeassistant-', '')); + await api.post('/homeassistant/events', { + calendar_id: haCalId, title, start, end, allDay, + location: loc, description: desc, + }); + showToast(t('event_created')); } else { const calId = parseInt(calVal); await api.post('/caldav/events', {