Stop one recurring event from taking its whole calendar down

An RRULE was parsed by splitting on ";" and "=", which leaves every value a
str. icalendar accepts that at add() time and only rejects it inside
to_ical(), where vRecur.types maps UNTIL to vDDDTypes and that demands a date.
So a single "repeats until <date>" event — exactly what the web editor writes —
made build_ics raise ValueError, and with it every REPORT, every event GET,
the whole-collection GET and the .ics export for that calendar.

PROPFIND never builds ICS, so discovery kept working. That is the asymmetry
behind the Home Assistant symptom: the calendar is found under its correct
name, then every update fails and the entity sits permanently unavailable. It
behaved identically through the token URL and the login URL because both reach
the same collection dispatch.

icalendar parses its own format correctly, so the rule now goes through
vRecur.from_ical. That parser can raise where the hand-rolled one silently
accepted anything, so an unreadable stored rule now drops the recurrence and
logs it instead of failing the calendar — the same tolerance the DTSTART and
DTEND parsing already has.

caldav_client carried a second copy of the same parser with the same defect,
used when pushing events to external CalDAV servers. Deleted; both directions
now share one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-25 16:59:41 +01:00
co-authored by Claude Opus 5
parent 28c24bc3cd
commit aa795fde76
3 changed files with 72 additions and 23 deletions
+15 -8
View File
@@ -135,13 +135,17 @@ def parse_ics(raw: bytes) -> dict:
def _rrule_str_to_vrecur(rrule_str: str) -> vRecur:
params = {}
for part in rrule_str.split(";"):
if "=" not in part:
continue
key, val = part.split("=", 1)
params[key] = val.split(",") if "," in val else val
return vRecur(params)
"""Parse an RRULE string into a vRecur with correctly typed values.
Hand-splitting on ";" and "=" leaves every value a str, which icalendar
accepts silently here and then rejects inside ``to_ical()``: vRecur.types
maps UNTIL to vDDDTypes, and that demands a date. A single "repeats until
<date>" event therefore made the whole calendar fail to serialise — a 500
on REPORT, on GET and on export, while PROPFIND kept working because it
never builds ICS. Letting icalendar parse its own format gives UNTIL as a
datetime and COUNT/INTERVAL as ints.
"""
return vRecur(vRecur.from_ical(rrule_str))
def _parse_iso(s: str) -> datetime:
@@ -198,7 +202,10 @@ def build_ics(calendar, events, *, name_cache: dict | None = None,
if ev.color:
item.add("x-calendarr-color", ev.color)
if ev.rrule:
item.add("rrule", _rrule_str_to_vrecur(ev.rrule))
try:
item.add("rrule", _rrule_str_to_vrecur(ev.rrule))
except (ValueError, TypeError):
logger.warning("Skipping unparsable RRULE %r on event %s", ev.rrule, ev.id)
# ORGANIZER from the creator (local user or imported name).
organizer_name = None