fix: HA Datetime-Format mit Timezone, leere Strings filtern, Debug-Logs

- _ha_format_dt: Parst ISO-Datetime zu datetime-Objekt, emittiert
  ohne Millisekunden, MIT Timezone-Offset. Vorher landeten Termine
  am falschen Datum, weil das Frontend UTC schickt aber wir die
  Timezone gestrippt haben → HA hat als lokale Zeit interpretiert
- Leere Strings werden nicht mehr in den Body aufgenommen (HA
  Validator könnte diese ablehnen)
- Logging in create/delete/update für besseres Debugging der HA-Calls
This commit is contained in:
Scarriffle
2026-04-29 19:50:52 +02:00
parent d942b82e1d
commit b803d4bf4c

View File

@@ -110,34 +110,36 @@ def _ha_get_events(url: str, token: str, entity_id: str, start_dt: datetime, end
def _ha_format_dt(s: str) -> str: def _ha_format_dt(s: str) -> str:
"""Convert ISO datetime to HA format: 'YYYY-MM-DD HH:MM:SS' (no timezone).""" """Convert ISO datetime to HA format with timezone, no milliseconds.
# Strip timezone if present
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"): if s.endswith("Z"):
s = s[:-1] s = s[:-1] + "+00:00"
# Strip +HH:MM or -HH:MM offset try:
for sep in ("+", "-"): dt = datetime.fromisoformat(s)
idx = s.rfind(sep) except ValueError:
if idx > 10: # Strip fractional seconds if fromisoformat can't handle them
s = s[:idx] import re
break s2 = re.sub(r"\.\d+", "", s)
# Replace T with space dt = datetime.fromisoformat(s2)
s = s.replace("T", " ") if dt.tzinfo is None:
# Ensure seconds are present dt = dt.replace(tzinfo=timezone.utc)
if len(s) == 16: # 'YYYY-MM-DD HH:MM' return dt.isoformat(timespec="seconds")
s += ":00"
return s
def _ha_build_event_body(entity_id: str, data: dict) -> dict: def _ha_build_event_body(entity_id: str, data: dict) -> dict:
"""Build a service-call body for create_event / update_event.""" """Build a service-call body for create_event / update_event."""
body = {"entity_id": entity_id} body = {"entity_id": entity_id}
if "title" in data: if data.get("title"):
body["summary"] = data["title"] body["summary"] = data["title"]
if "description" in data: if data.get("description"):
body["description"] = data["description"] body["description"] = data["description"]
if "location" in data: if data.get("location"):
body["location"] = data["location"] body["location"] = data["location"]
if "start" in data and "end" in data: if data.get("start") and data.get("end"):
if data.get("allDay"): if data.get("allDay"):
body["start_date"] = data["start"][:10] body["start_date"] = data["start"][:10]
body["end_date"] = data["end"][:10] body["end_date"] = data["end"][:10]
@@ -152,6 +154,7 @@ def _ha_create_event(url: str, token: str, entity_id: str, data: dict) -> dict:
base = url.rstrip("/") base = url.rstrip("/")
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
body = _ha_build_event_body(entity_id, data) body = _ha_build_event_body(entity_id, data)
logger.info("HA create_event body: %s", body)
resp = http_requests.post( resp = http_requests.post(
f"{base}/api/services/calendar/create_event", f"{base}/api/services/calendar/create_event",
headers=headers, json=body, timeout=15, verify=False, headers=headers, json=body, timeout=15, verify=False,
@@ -211,15 +214,18 @@ def _ha_delete_event(url: str, token: str, entity_id: str, uid: str):
"""Delete an event via HA service call API (calendar.delete_event).""" """Delete an event via HA service call API (calendar.delete_event)."""
base = url.rstrip("/") base = url.rstrip("/")
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} 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( resp = http_requests.post(
f"{base}/api/services/calendar/delete_event", f"{base}/api/services/calendar/delete_event",
headers=headers, headers=headers, json=body, timeout=15, verify=False,
json={"entity_id": entity_id, "uid": uid},
timeout=15, verify=False,
) )
if not resp.ok: if not resp.ok:
detail = resp.text[:500] if resp.text else f"HTTP {resp.status_code}" try:
raise Exception(f"HA delete_event: {resp.status_code}{detail}") 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 delete_event ({resp.status_code}): {detail}")
return resp return resp