Add event status to calendar events - #179312
Conversation
Add CalendarEventStatus for the rfc5545 STATUS property, expose it on CalendarEvent, and populate it from caldav, local_calendar and google.
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds first-class support for calendar event status (RFC5545 STATUS) across multiple calendar integrations and updates tests/snapshots to validate the new field.
Changes:
- Introduces
CalendarEventStatusand addsstatustoCalendarEvent+ API field constants. - Populates
statusin Local Calendar, CalDAV, and Google calendar event conversion paths. - Updates component tests and snapshot expectations to include
status.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
homeassistant/components/calendar/const.py |
Adds CalendarEventStatus enum and EVENT_STATUS field constant to exposed event fields. |
homeassistant/components/calendar/__init__.py |
Extends CalendarEvent dataclass with status. |
homeassistant/components/local_calendar/calendar.py |
Maps iCalendar library event status into CalendarEventStatus. |
homeassistant/components/caldav/coordinator.py |
Parses VEVENT STATUS into CalendarEventStatus with fallback for unsupported tokens. |
homeassistant/components/google/calendar.py |
Maps Google event status into CalendarEventStatus. |
tests/components/local_calendar/test_calendar.py |
Adds a new parametrized test for RFC5545 STATUS. |
tests/components/caldav/test_calendar.py |
Refactors test setup helper and adds new tests for VEVENT STATUS. |
tests/components/google/test_calendar.py |
Adds HTTP API test coverage for event status behavior. |
tests/components/*/snapshots/test_calendar.ambr |
Updates snapshots to include status: None in event payloads. |
tests/components/todoist/test_calendar.py |
Updates expected event dict to include status. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| @pytest.mark.parametrize( | ||
| ("ics_content", "expected_status"), | ||
| [ | ||
| pytest.param( | ||
| _ics_with_status("STATUS:CANCELLED\n"), "cancelled", id="cancelled" | ||
| ), | ||
| pytest.param( | ||
| _ics_with_status("STATUS:TENTATIVE\n"), "tentative", id="tentative" | ||
| ), | ||
| pytest.param( | ||
| _ics_with_status("STATUS:CONFIRMED\n"), "confirmed", id="confirmed" | ||
| ), | ||
| pytest.param(_ics_with_status(""), None, id="no_status_property"), | ||
| ], | ||
| ) | ||
| @pytest.mark.usefixtures("setup_integration") | ||
| async def test_event_status( | ||
| get_events: GetEventsFn, | ||
| expected_status: str | None, | ||
| ) -> None: |
There was a problem hiding this comment.
ics_content is a fixture defined in this package's conftest.py and marked autouse. Parametrizing by a fixture name overrides that fixture's value, which does not require the test to declare it as an argument — the function uses no argument error only applies to names that are not fixtures. The store fixture consumes it to seed the calendar. All four cases collect and pass.
| SUMMARY:This is an event with a status | ||
| LOCATION:Hamburg | ||
| DESCRIPTION:Surprisingly rainy | ||
| {status_property}END:VEVENT |
There was a problem hiding this comment.
The placeholder is on its own line in the f-string, so it does not extend the DESCRIPTION line. With an empty status_property the event ends:
DESCRIPTION:Surprisingly rainy
END:VEVENT
and with a value set, STATUS:CANCELLED sits between the two. The no_status_property case also asserts that exactly one event is returned, which would not hold if the iCalendar were malformed.
| ] | ||
|
|
||
|
|
||
| def _ics_with_status(status_property: str) -> str: |
There was a problem hiding this comment.
I'd say:
(1) just make this a constant ICS_WITH_STATUS
(2) limit what is in the template: STATUS:{status}\n
(3) parameterize by the status string only and call .format on the constant to get the ics content
We can exercise the default in an existing test that doesn't set status.
There was a problem hiding this comment.
Done in 5ffaf25 — constant plus .format, and the default is still covered by test_get_events_custom_calendars.
|
|
||
| CONFIRMED = "confirmed" | ||
| TENTATIVE = "tentative" | ||
| CANCELLED = "cancelled" |
There was a problem hiding this comment.
My understanding of our discussion was that we agreed that integrations would not return cancelled events. I think this needs to be stated somewhere since the frontend won't handle them properly.
Generally, I'd like to see the "spec" part of this ironed out and reflected in the developer docs as well.
There was a problem hiding this comment.
Both caldav and local_calendar already return cancelled events today, before this PR. ical's timeline does not filter on status, and the caldav coordinator does not read STATUS at all. So this change does not make cancelled events appear — it only makes them identifiable. Dropping cancelled from the enum would leave the same events being returned, just with no way for a consumer to tell them apart.
I think the 2022 discussion was reasoning about integrations that treat cancelled as deleted, which is how the Google API defines it. The two libraries already encode that distinction: gcal_sync drops cancelled events when building the timeline, ical keeps them. So google will only ever report confirmed or tentative, while in RFC 5545 a cancelled event deliberately stays in the calendar — my Nextcloud keeps it rather than deleting it.
That makes the spec a layering question: the integration reports what the calendar reports, and the consumer decides how to present it. Filtering in the integration would remove events that Home Assistant shows today, and it would put a presentation decision in the data layer.
On the frontend: that is deliberately out of scope here, since it lives in its own repository and is a separate change. This field is what would make it possible to handle cancelled events there at all — today the panel has no way to tell one from an ordinary event.
Happy to write this up in the developer documentation PR. If you would rather integrations filter cancelled events, that is a behavior change for caldav and local_calendar, and I would want to do that as a separate PR.
There was a problem hiding this comment.
I've written the spec into the developer docs PR: home-assistant/developers.home-assistant#3300. It states that an integration reports what its calendar reports and does not hide events based on status, and documents why google can only report confirmed or tentative.
There was a problem hiding this comment.
OK you're right, but let me be more specific about local_calendar: The integration was originally made for local events, so it never allowed creating cancelled events.
Then people extended the local calendar integration to allow uploading ics and didn't add any filtering. Remote calendar also didn't consider this. This was not some intentional feature the authors intended to support, they just didn't consider it.
So again: We said in the architecture issue we did not want to have integrations returning cancelled events and have a cancelled status (Independent of what the integrations do literally today). That is not a status we would want to expose in the API. If you need help understanding what to do next let me know and i can help you.
There was a problem hiding this comment.
OK just to save time and be more prescriptive: I would say send separate PRs to update integrations to filter cancelled events. We won't have events in the calendar entity with a canceled status as we already agreed in the discussion.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
homeassistant/components/local_calendar/calendar.py:248
CalendarEventStatus(...)will raiseValueErrorif an unexpected/unsupported status value ever comes through. That would break event parsing and potentially the calendar entity update. Prefer a defensive mapping (e.g., wrap intry/except ValueErrorand returnNonewith a debug log), similar to the CalDAV coordinator’s handling.
# ical validates STATUS against the same rfc5545 set, so the mapping is
# total and needs no fallback. Its values are upper case, and its enum
# is a plain (str, Enum) rather than a StrEnum, so str() would yield
# "EventStatus.CONFIRMED" instead of the value.
status=(
CalendarEventStatus(event.status.value.lower()) if event.status else None
),
homeassistant/components/caldav/coordinator.py:31
- The
veventparameter type annotation looks incorrect/misleading: the function treats it like a VEVENT/vobject component (passed toget_attr_value), not acaldav.CalendarObjectResource. Updating the annotation to the actual VEVENT type used in this module (orAnyif there isn’t a good exported type) will improve readability and static analysis.
def _get_status(vevent: caldav.CalendarObjectResource) -> CalendarEventStatus | None:
"""Return the rfc5545 STATUS of a VEVENT, if it is one we know.
tests/components/caldav/test_calendar.py:400
veventis a bit misleading here: the test data passed in appears to be an entire VCALENDAR payload containing a VEVENT (not just the VEVENT stanza). Consider renaming the parameter to something likeicalendar_payload/vcalendar(and adjusting the docstring accordingly) to make its expected content clearer.
async def _get_api_events_for_vevent(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
vevent: str,
uid: str,
) -> list[dict[str, Any]]:
"""Set up a calendar holding a single VEVENT and return its events from the API.
| @pytest.mark.usefixtures("setup_integration") | ||
| async def test_event_status( | ||
| get_events: GetEventsFn, |
There was a problem hiding this comment.
Same as the earlier thread: ics_content is a fixture in this package's conftest.py, so parametrizing by that name overrides the fixture value rather than injecting an argument. The three cases collect and pass.
The suggested change would also add two arguments the test body does not use. The project docs ask for the opposite: "Prefer @pytest.mark.usefixtures over arguments, if the argument is not going to be used."
|
|
||
| CONFIRMED = "confirmed" | ||
| TENTATIVE = "tentative" | ||
| CANCELLED = "cancelled" |
There was a problem hiding this comment.
OK just to save time and be more prescriptive: I would say send separate PRs to update integrations to filter cancelled events. We won't have events in the calendar entity with a canceled status as we already agreed in the discussion.
| @pytest.mark.usefixtures("setup_integration") | ||
| async def test_event_status( | ||
| get_events: GetEventsFn, | ||
| expected_status: str | None, | ||
| ) -> None: | ||
| """Test that the rfc5545 STATUS property is returned by the API.""" |
|
Understood — reduced to |
Add CalendarEventStatus for the RFC 5545 STATUS property, expose it on CalendarEvent, and populate it from caldav, local_calendar and google.
Proposed change
This implements the decision from architecture#884, which was approved but never carried out.
CalendarEventgains an optionalstatusfield carrying the RFC 5545STATUSproperty, typed as a newCalendarEventStatusenum withconfirmed,tentative, andcancelled. It is populated by caldav, local_calendar, and google — the three integrations named as a condition in that discussion. An earlier attempt (#77917) added the field but populated it only in the demo integration, and was closed.I ran into this with my own Nextcloud calendar, where an event that is marked as cancelled stays in the calendar with
STATUS:CANCELLEDrather than being deleted. Home Assistant currently drops that distinction, so the event shows up as if it were still going ahead.A few notes on the implementation:
icalandgcal_syncalready validate against the same set.as_dict()and omits the field when it is unset.Google is a special case worth calling out: in its API
cancelledmeans deleted, andgcal_syncdrops those events when building the timeline. The integration will therefore only ever reportconfirmedortentative. That is intentional and left as is.The field is additive, so nothing changes for existing consumers. Several tests in other calendar integrations assert the complete API payload and were updated to include the new field; no existing value changed.
I found the earlier discussion and approval, and thought I'd pick it up — a small way to give something back to a project I use daily.
Type of change
Additional information
Checklist
ruff format homeassistant tests)If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
Updated and included derived files by running:
python3 -m script.hassfest.requirements_all.txt.Updated by running
python3 -m script.gen_requirements_all.To help with the load of incoming pull requests: