Skip to content

Add event status to calendar events - #179312

Open
1358 wants to merge 3 commits into
home-assistant:devfrom
1358:calendar-event-status
Open

Add event status to calendar events#179312
1358 wants to merge 3 commits into
home-assistant:devfrom
1358:calendar-event-status

Conversation

@1358

@1358 1358 commented Aug 16, 2026

Copy link
Copy Markdown

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.

CalendarEvent gains an optional status field carrying the RFC 5545 STATUS property, typed as a new CalendarEventStatus enum with confirmed, tentative, and cancelled. 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:CANCELLED rather 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:

  • The field is an enum rather than a free-form string, so integrations cannot drift into different spellings of the same value.
  • caldav ignores values outside the RFC 5545 set, since RFC 5545 also permits iana-tokens and x-names there. local_calendar and google map directly, because ical and gcal_sync already validate against the same set.
  • State attributes are unchanged. Automations get the status through the trigger payload, which is built by as_dict() and omits the field when it is unset.

Google is a special case worth calling out: in its API cancelled means deleted, and gcal_sync drops those events when building the timeline. The integration will therefore only ever report confirmed or tentative. 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

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

Add CalendarEventStatus for the rfc5545 STATUS property, expose it on
CalendarEvent, and populate it from caldav, local_calendar and google.
Copilot AI balanced review requested due to automatic review settings August 16, 2026 14:29
@1358
1358 requested review from a team, allenporter, frenck and joostlek as code owners August 16, 2026 14:29

@home-assistant home-assistant Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @1358

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

@home-assistant

Copy link
Copy Markdown
Contributor

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 CalendarEventStatus and adds status to CalendarEvent + API field constants.
  • Populates status in 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.

Comment on lines +1182 to +1201
@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:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@1358 1358 Aug 16, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5ffaf25 — constant plus .format, and the default is still covered by test_get_events_custom_calendars.


CONFIRMED = "confirmed"
TENTATIVE = "tentative"
CANCELLED = "cancelled"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@1358 1358 Aug 16, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@home-assistant
home-assistant Bot marked this pull request as draft August 16, 2026 16:39
Copilot AI review requested due to automatic review settings August 16, 2026 16:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 raise ValueError if 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 in try/except ValueError and return None with 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 vevent parameter type annotation looks incorrect/misleading: the function treats it like a VEVENT/vobject component (passed to get_attr_value), not a caldav.CalendarObjectResource. Updating the annotation to the actual VEVENT type used in this module (or Any if 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

  • vevent is 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 like icalendar_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.

Comment on lines +1188 to +1190
@pytest.mark.usefixtures("setup_integration")
async def test_event_status(
get_events: GetEventsFn,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."

@1358
1358 marked this pull request as ready for review August 16, 2026 17:57
@home-assistant
home-assistant Bot requested a review from allenporter August 16, 2026 17:57

CONFIRMED = "confirmed"
TENTATIVE = "tentative"
CANCELLED = "cancelled"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@home-assistant
home-assistant Bot marked this pull request as draft August 16, 2026 18:21
@1358 1358 changed the title Add RFC 5545 status to calendar events Add event status to calendar events Aug 16, 2026
Copilot AI review requested due to automatic review settings August 16, 2026 18:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Comment on lines +1190 to +1195
@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."""
@1358

1358 commented Aug 16, 2026

Copy link
Copy Markdown
Author

Understood — reduced to confirmed and tentative, and both documentation PRs updated to match. The cancelled value now maps to no status, with a test pinning that.

@1358
1358 marked this pull request as ready for review August 16, 2026 19:12
@home-assistant
home-assistant Bot requested a review from allenporter August 16, 2026 19:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants