calendar.delete_event schlägt mit 400 fehl, wenn die HA-Integration das Feature nicht unterstützt (z.B. Google-Calendar via HA hat nur CREATE_EVENT, kein DELETE/UPDATE). - Versucht erst Service-Call, dann REST DELETE als Fallback - Bei 400 wird der User aufgeklärt, dass die Integration vermutlich kein Löschen unterstützt
688 lines
22 KiB
Python
688 lines
22 KiB
Python
import logging
|
||
import secrets
|
||
import time
|
||
from datetime import datetime, timezone
|
||
from typing import Optional
|
||
from urllib.parse import urlencode
|
||
|
||
import requests as http_requests
|
||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||
from fastapi.responses import RedirectResponse
|
||
from pydantic import BaseModel
|
||
from sqlalchemy.orm import Session
|
||
|
||
import models
|
||
from auth import get_current_user
|
||
from database import get_db
|
||
|
||
logger = logging.getLogger(__name__)
|
||
router = APIRouter()
|
||
|
||
HA_DEFAULT_COLOR = "#03a9f4"
|
||
|
||
# In-memory store for pending OAuth states (short-lived, ~10 min TTL)
|
||
_pending_oauth: dict[str, dict] = {}
|
||
|
||
|
||
def _cleanup_pending():
|
||
now = time.time()
|
||
for k in [k for k, v in _pending_oauth.items() if v["expires"] < now]:
|
||
_pending_oauth.pop(k, None)
|
||
|
||
|
||
# ── Auth helpers ──────────────────────────────────────────
|
||
|
||
def _ha_refresh(url: str, refresh_token: str, client_id: str) -> tuple:
|
||
"""Refresh grant → (access_token, expires_in)"""
|
||
resp = http_requests.post(
|
||
f"{url.rstrip('/')}/auth/token",
|
||
data={
|
||
"grant_type": "refresh_token",
|
||
"refresh_token": refresh_token,
|
||
"client_id": client_id,
|
||
},
|
||
timeout=10,
|
||
verify=False,
|
||
)
|
||
resp.raise_for_status()
|
||
data = resp.json()
|
||
return data["access_token"], data.get("expires_in", 1800)
|
||
|
||
|
||
def _get_valid_token(account: models.HomeAssistantAccount, db: Session) -> str:
|
||
"""Return a valid access token, refreshing if necessary."""
|
||
if account.auth_method != "oauth":
|
||
return account.token # Long-Lived Token läuft nicht ab
|
||
now = datetime.now(timezone.utc)
|
||
if account.token_expiry and account.token_expiry.replace(tzinfo=timezone.utc) > now:
|
||
return account.token
|
||
# Needs refresh
|
||
try:
|
||
access_token, expires_in = _ha_refresh(
|
||
account.url, account.refresh_token, account.client_id or ""
|
||
)
|
||
except Exception as exc:
|
||
logger.error("HA token refresh failed for %s: %s", account.name, exc)
|
||
raise HTTPException(401, "Home Assistant Token abgelaufen, bitte Konto neu verbinden")
|
||
account.token = access_token
|
||
account.token_expiry = datetime.fromtimestamp(now.timestamp() + expires_in, tz=timezone.utc)
|
||
db.commit()
|
||
return access_token
|
||
|
||
|
||
# ── HA API helpers ────────────────────────────────────────
|
||
|
||
def _ha_get_calendars(url: str, token: str) -> list:
|
||
try:
|
||
resp = http_requests.get(
|
||
f"{url.rstrip('/')}/api/calendars",
|
||
headers={"Authorization": f"Bearer {token}"},
|
||
timeout=10,
|
||
verify=False,
|
||
)
|
||
resp.raise_for_status()
|
||
return resp.json()
|
||
except http_requests.exceptions.ConnectionError:
|
||
raise HTTPException(503, "Home Assistant nicht erreichbar")
|
||
except http_requests.exceptions.Timeout:
|
||
raise HTTPException(503, "Home Assistant antwortet nicht (Timeout)")
|
||
except http_requests.exceptions.HTTPError as e:
|
||
if e.response is not None and e.response.status_code == 401:
|
||
raise HTTPException(400, "Ungültiger Access Token")
|
||
raise HTTPException(502, f"Home Assistant Fehler: {e}")
|
||
|
||
|
||
def _ha_get_events(url: str, token: str, entity_id: str, start_dt: datetime, end_dt: datetime) -> list:
|
||
try:
|
||
resp = http_requests.get(
|
||
f"{url.rstrip('/')}/api/calendars/{entity_id}",
|
||
headers={"Authorization": f"Bearer {token}"},
|
||
params={"start": start_dt.isoformat(), "end": end_dt.isoformat()},
|
||
timeout=15,
|
||
verify=False,
|
||
)
|
||
resp.raise_for_status()
|
||
return resp.json()
|
||
except http_requests.exceptions.ConnectionError:
|
||
raise http_requests.exceptions.ConnectionError(f"HA nicht erreichbar für {entity_id}")
|
||
except http_requests.exceptions.Timeout:
|
||
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 with timezone, no milliseconds.
|
||
|
||
HA's cv.datetime accepts ISO 8601. Keep the timezone offset so HA
|
||
interprets the time correctly regardless of HA's local timezone.
|
||
"""
|
||
# Frontend sends "2026-05-07T15:00:00.000Z"; normalize Z to +00:00
|
||
if s.endswith("Z"):
|
||
s = s[:-1] + "+00:00"
|
||
try:
|
||
dt = datetime.fromisoformat(s)
|
||
except ValueError:
|
||
# Strip fractional seconds if fromisoformat can't handle them
|
||
import re
|
||
s2 = re.sub(r"\.\d+", "", s)
|
||
dt = datetime.fromisoformat(s2)
|
||
if dt.tzinfo is None:
|
||
dt = dt.replace(tzinfo=timezone.utc)
|
||
return dt.isoformat(timespec="seconds")
|
||
|
||
|
||
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}
|
||
if data.get("title"):
|
||
body["summary"] = data["title"]
|
||
if data.get("description"):
|
||
body["description"] = data["description"]
|
||
if data.get("location"):
|
||
body["location"] = data["location"]
|
||
if data.get("start") and data.get("end"):
|
||
if data.get("allDay"):
|
||
body["start_date"] = data["start"][:10]
|
||
body["end_date"] = data["end"][:10]
|
||
else:
|
||
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)
|
||
logger.info("HA create_event body: %s", body)
|
||
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("/")
|
||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||
|
||
# Try update_event service (HA 2024.6+)
|
||
body = _ha_build_event_body(entity_id, data)
|
||
body["uid"] = uid
|
||
resp = http_requests.post(
|
||
f"{base}/api/services/calendar/update_event",
|
||
headers=headers, json=body, timeout=15, verify=False,
|
||
)
|
||
if resp.ok:
|
||
return resp
|
||
|
||
logger.info("HA update_event not available (%s), falling back to delete+create", resp.status_code)
|
||
|
||
# Fallback: delete old event, then create new one
|
||
del_resp = http_requests.post(
|
||
f"{base}/api/services/calendar/delete_event",
|
||
headers=headers,
|
||
json={"entity_id": entity_id, "uid": uid},
|
||
timeout=15, verify=False,
|
||
)
|
||
if not del_resp.ok:
|
||
logger.warning("HA delete_event failed (%s): %s", del_resp.status_code, del_resp.text[:200])
|
||
# If delete also fails, try create anyway (might duplicate)
|
||
|
||
create_body = _ha_build_event_body(entity_id, data)
|
||
create_resp = http_requests.post(
|
||
f"{base}/api/services/calendar/create_event",
|
||
headers=headers, json=create_body, timeout=15, verify=False,
|
||
)
|
||
if not create_resp.ok:
|
||
try:
|
||
detail = create_resp.json().get("message", create_resp.text[:500])
|
||
except Exception:
|
||
detail = create_resp.text[:500] if create_resp.text else str(create_resp.status_code)
|
||
raise Exception(f"HA create_event ({create_resp.status_code}): {detail}")
|
||
return create_resp
|
||
|
||
|
||
def _ha_delete_event(url: str, token: str, entity_id: str, uid: str):
|
||
"""Delete an event via HA service call API (calendar.delete_event)."""
|
||
base = url.rstrip("/")
|
||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||
body = {"entity_id": entity_id, "uid": uid}
|
||
logger.info("HA delete_event body: %s", body)
|
||
resp = http_requests.post(
|
||
f"{base}/api/services/calendar/delete_event",
|
||
headers=headers, json=body, timeout=15, verify=False,
|
||
)
|
||
if resp.ok:
|
||
return resp
|
||
|
||
# Try REST API DELETE as fallback (works for some integrations)
|
||
from urllib.parse import quote
|
||
encoded_uid = quote(uid, safe="")
|
||
rest_resp = http_requests.delete(
|
||
f"{base}/api/calendars/{entity_id}/{encoded_uid}",
|
||
headers={"Authorization": f"Bearer {token}"},
|
||
timeout=15, verify=False,
|
||
)
|
||
if rest_resp.ok:
|
||
return rest_resp
|
||
|
||
# Both failed – build a helpful error message
|
||
try:
|
||
detail = resp.json().get("message", resp.text[:500])
|
||
except Exception:
|
||
detail = resp.text[:500] if resp.text else f"HTTP {resp.status_code}"
|
||
if resp.status_code == 400:
|
||
detail = f"{detail} (Diese HA-Kalender-Integration unterstützt kein Löschen — z.B. Google-Calendar via HA ist read-only für Updates/Löschen)"
|
||
raise Exception(f"HA delete_event ({resp.status_code}): {detail}")
|
||
|
||
|
||
def _parse_ha_event(ev: dict, cal_db_id: int, cal_name: str, cal_color: str) -> dict:
|
||
start = ev.get("start", {})
|
||
end = ev.get("end", {})
|
||
all_day = "date" in start and "dateTime" not in start
|
||
return {
|
||
"id": ev.get("uid") or f"ha-{cal_db_id}-{ev.get('summary', '')}",
|
||
"url": f"homeassistant://{cal_db_id}/{ev.get('uid', '')}",
|
||
"title": ev.get("summary", "(Kein Titel)"),
|
||
"start": start.get("dateTime") or start.get("date", ""),
|
||
"end": end.get("dateTime") or end.get("date", ""),
|
||
"allDay": all_day,
|
||
"location": ev.get("location", ""),
|
||
"description": ev.get("description", ""),
|
||
"color": None,
|
||
"calendar_id": f"homeassistant-{cal_db_id}",
|
||
"calendar_name": cal_name,
|
||
"calendarColor": cal_color,
|
||
"source": "homeassistant",
|
||
}
|
||
|
||
|
||
def get_ha_events(account: models.HomeAssistantAccount, start_dt: datetime, end_dt: datetime, db: Session) -> list:
|
||
all_events = []
|
||
try:
|
||
token = _get_valid_token(account, db)
|
||
except Exception as exc:
|
||
logger.error("HA token error for %s: %s", account.name, exc)
|
||
raise
|
||
for cal in account.calendars:
|
||
if not cal.enabled or cal.sidebar_hidden:
|
||
continue
|
||
try:
|
||
raw = _ha_get_events(account.url, token, cal.entity_id, start_dt, end_dt)
|
||
color = cal.color or HA_DEFAULT_COLOR
|
||
for ev in raw:
|
||
all_events.append(_parse_ha_event(ev, cal.id, cal.name, color))
|
||
except Exception as exc:
|
||
logger.error("HA event fetch error %s (%s): %s", cal.entity_id, account.name, exc)
|
||
return all_events
|
||
|
||
|
||
# ── Serialization ─────────────────────────────────────────
|
||
|
||
def _account_dict(a: models.HomeAssistantAccount) -> dict:
|
||
return {
|
||
"id": a.id,
|
||
"name": a.name,
|
||
"url": a.url,
|
||
"auth_method": a.auth_method or "token",
|
||
"calendars": [
|
||
{
|
||
"id": c.id,
|
||
"name": c.name,
|
||
"entity_id": c.entity_id,
|
||
"color": c.color or HA_DEFAULT_COLOR,
|
||
"enabled": c.enabled,
|
||
"sidebar_hidden": bool(c.sidebar_hidden),
|
||
}
|
||
for c in a.calendars
|
||
],
|
||
}
|
||
|
||
|
||
# ── Pydantic models ───────────────────────────────────────
|
||
|
||
class HAAccountCreate(BaseModel):
|
||
name: str
|
||
url: str
|
||
token: str
|
||
|
||
|
||
class HAOAuthStart(BaseModel):
|
||
name: str
|
||
url: str
|
||
client_id: str
|
||
redirect_uri: str
|
||
|
||
|
||
class HACalendarUpdate(BaseModel):
|
||
enabled: Optional[bool] = None
|
||
color: Optional[str] = None
|
||
name: Optional[str] = None
|
||
sidebar_hidden: Optional[bool] = None
|
||
|
||
|
||
# ── Endpoints ─────────────────────────────────────────────
|
||
|
||
@router.get("/accounts")
|
||
def list_accounts(
|
||
db: Session = Depends(get_db),
|
||
current_user: models.User = Depends(get_current_user),
|
||
):
|
||
accounts = (
|
||
db.query(models.HomeAssistantAccount)
|
||
.filter(models.HomeAssistantAccount.user_id == current_user.id)
|
||
.all()
|
||
)
|
||
return [_account_dict(a) for a in accounts]
|
||
|
||
|
||
@router.post("/accounts")
|
||
def add_account(
|
||
data: HAAccountCreate,
|
||
db: Session = Depends(get_db),
|
||
current_user: models.User = Depends(get_current_user),
|
||
):
|
||
"""Create a HA account from a Long-Lived Access Token."""
|
||
remote_cals = _ha_get_calendars(data.url, data.token)
|
||
|
||
account = models.HomeAssistantAccount(
|
||
user_id=current_user.id,
|
||
name=data.name,
|
||
url=data.url,
|
||
token=data.token,
|
||
auth_method="token",
|
||
refresh_token=None,
|
||
token_expiry=None,
|
||
)
|
||
db.add(account)
|
||
db.flush()
|
||
|
||
for cal in remote_cals:
|
||
entity_id = cal.get("entity_id", "")
|
||
if not entity_id:
|
||
continue
|
||
db.add(models.HomeAssistantCalendar(
|
||
account_id=account.id,
|
||
entity_id=entity_id,
|
||
name=cal.get("name") or entity_id,
|
||
color=None,
|
||
enabled=True,
|
||
))
|
||
|
||
db.commit()
|
||
db.refresh(account)
|
||
return _account_dict(account)
|
||
|
||
|
||
@router.post("/auth-url")
|
||
def oauth_start(
|
||
data: HAOAuthStart,
|
||
current_user: models.User = Depends(get_current_user),
|
||
):
|
||
"""Start the OAuth flow: store pending state, return HA authorization URL."""
|
||
_cleanup_pending()
|
||
state_token = secrets.token_urlsafe(32)
|
||
_pending_oauth[state_token] = {
|
||
"user_id": current_user.id,
|
||
"ha_url": data.url.rstrip('/'),
|
||
"name": data.name,
|
||
"client_id": data.client_id,
|
||
"redirect_uri": data.redirect_uri,
|
||
"expires": time.time() + 600,
|
||
}
|
||
params = {
|
||
"client_id": data.client_id,
|
||
"redirect_uri": data.redirect_uri,
|
||
"state": state_token,
|
||
"response_type": "code",
|
||
}
|
||
return {"url": f"{data.url.rstrip('/')}/auth/authorize?{urlencode(params)}"}
|
||
|
||
|
||
@router.get("/callback")
|
||
def oauth_callback(
|
||
request: Request,
|
||
code: str = Query(""),
|
||
state: str = Query(""),
|
||
error: str = Query(""),
|
||
db: Session = Depends(get_db),
|
||
):
|
||
"""Callback from Home Assistant after user authorization."""
|
||
if error or not code:
|
||
return RedirectResponse(url=f"/?ha_error={error or 'no_code'}", status_code=302)
|
||
|
||
pending = _pending_oauth.pop(state, None)
|
||
if not pending or pending["expires"] < time.time():
|
||
return RedirectResponse(url="/?ha_error=state_expired", status_code=302)
|
||
|
||
ha_url = pending["ha_url"]
|
||
client_id = pending["client_id"]
|
||
|
||
# Exchange code for tokens
|
||
try:
|
||
resp = http_requests.post(
|
||
f"{ha_url}/auth/token",
|
||
data={
|
||
"grant_type": "authorization_code",
|
||
"code": code,
|
||
"client_id": client_id,
|
||
},
|
||
timeout=15,
|
||
verify=False,
|
||
)
|
||
except Exception as exc:
|
||
logger.error("HA token exchange connection error: %s", exc)
|
||
return RedirectResponse(url="/?ha_error=ha_unreachable", status_code=302)
|
||
|
||
if resp.status_code != 200:
|
||
logger.error("HA token exchange failed (%s): %s", resp.status_code, resp.text)
|
||
return RedirectResponse(url="/?ha_error=token_exchange_failed", status_code=302)
|
||
|
||
tokens = resp.json()
|
||
access_token = tokens["access_token"]
|
||
refresh_token = tokens.get("refresh_token", "")
|
||
expires_in = tokens.get("expires_in", 1800)
|
||
now = datetime.now(timezone.utc)
|
||
|
||
try:
|
||
remote_cals = _ha_get_calendars(ha_url, access_token)
|
||
except HTTPException as exc:
|
||
logger.error("HA calendar fetch failed after OAuth: %s", exc.detail)
|
||
return RedirectResponse(url="/?ha_error=calendars_failed", status_code=302)
|
||
|
||
account = models.HomeAssistantAccount(
|
||
user_id=pending["user_id"],
|
||
name=pending["name"],
|
||
url=ha_url,
|
||
token=access_token,
|
||
auth_method="oauth",
|
||
refresh_token=refresh_token,
|
||
token_expiry=datetime.fromtimestamp(now.timestamp() + expires_in, tz=timezone.utc),
|
||
client_id=client_id,
|
||
)
|
||
db.add(account)
|
||
db.flush()
|
||
|
||
for cal in remote_cals:
|
||
entity_id = cal.get("entity_id", "")
|
||
if not entity_id:
|
||
continue
|
||
db.add(models.HomeAssistantCalendar(
|
||
account_id=account.id,
|
||
entity_id=entity_id,
|
||
name=cal.get("name") or entity_id,
|
||
color=None,
|
||
enabled=True,
|
||
))
|
||
|
||
db.commit()
|
||
return RedirectResponse(url="/?ha_connected=1", status_code=302)
|
||
|
||
|
||
@router.delete("/accounts/{account_id}")
|
||
def delete_account(
|
||
account_id: int,
|
||
db: Session = Depends(get_db),
|
||
current_user: models.User = Depends(get_current_user),
|
||
):
|
||
acc = (
|
||
db.query(models.HomeAssistantAccount)
|
||
.filter(
|
||
models.HomeAssistantAccount.id == account_id,
|
||
models.HomeAssistantAccount.user_id == current_user.id,
|
||
)
|
||
.first()
|
||
)
|
||
if not acc:
|
||
raise HTTPException(404, "Account not found")
|
||
db.delete(acc)
|
||
db.commit()
|
||
return {"ok": True}
|
||
|
||
|
||
@router.post("/accounts/{account_id}/sync")
|
||
def sync_account(
|
||
account_id: int,
|
||
db: Session = Depends(get_db),
|
||
current_user: models.User = Depends(get_current_user),
|
||
):
|
||
acc = (
|
||
db.query(models.HomeAssistantAccount)
|
||
.filter(
|
||
models.HomeAssistantAccount.id == account_id,
|
||
models.HomeAssistantAccount.user_id == current_user.id,
|
||
)
|
||
.first()
|
||
)
|
||
if not acc:
|
||
raise HTTPException(404, "Account not found")
|
||
|
||
token = _get_valid_token(acc, db)
|
||
remote_cals = _ha_get_calendars(acc.url, token)
|
||
existing = {c.entity_id: c for c in acc.calendars}
|
||
|
||
for cal in remote_cals:
|
||
entity_id = cal.get("entity_id", "")
|
||
if not entity_id:
|
||
continue
|
||
if entity_id not in existing:
|
||
db.add(models.HomeAssistantCalendar(
|
||
account_id=acc.id,
|
||
entity_id=entity_id,
|
||
name=cal.get("name") or entity_id,
|
||
color=None,
|
||
enabled=True,
|
||
))
|
||
else:
|
||
existing[entity_id].name = cal.get("name") or entity_id
|
||
|
||
db.commit()
|
||
db.refresh(acc)
|
||
return _account_dict(acc)
|
||
|
||
|
||
@router.put("/calendars/{calendar_id}")
|
||
def update_calendar(
|
||
calendar_id: int,
|
||
data: HACalendarUpdate,
|
||
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 == calendar_id,
|
||
models.HomeAssistantAccount.user_id == current_user.id,
|
||
)
|
||
.first()
|
||
)
|
||
if not cal:
|
||
raise HTTPException(404, "Calendar not found")
|
||
if data.enabled is not None:
|
||
cal.enabled = data.enabled
|
||
if data.color is not None:
|
||
cal.color = data.color
|
||
if data.name is not None:
|
||
cal.name = data.name
|
||
if data.sidebar_hidden is not None:
|
||
cal.sidebar_hidden = data.sidebar_hidden
|
||
db.commit()
|
||
return {"ok": True}
|
||
|
||
|
||
# ── Event CRUD ───────────────────────────────────────────
|
||
|
||
class HAEventUpdate(BaseModel):
|
||
title: Optional[str] = None
|
||
start: Optional[str] = None
|
||
end: Optional[str] = None
|
||
allDay: Optional[bool] = None
|
||
location: Optional[str] = None
|
||
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,
|
||
uid: str,
|
||
data: HAEventUpdate,
|
||
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 == 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_update_event(
|
||
account.url, token, cal.entity_id, uid,
|
||
data.model_dump(exclude_none=True),
|
||
)
|
||
return {"ok": True}
|
||
except Exception as exc:
|
||
raise HTTPException(500, f"HA event update failed: {exc}")
|
||
|
||
|
||
@router.delete("/events/{calendar_id}/{uid}")
|
||
def delete_event(
|
||
calendar_id: int,
|
||
uid: str,
|
||
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 == 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_delete_event(account.url, token, cal.entity_id, uid)
|
||
return {"ok": True}
|
||
except Exception as exc:
|
||
raise HTTPException(500, f"HA event delete failed: {exc}")
|