Add event status to calendar events (#179312)

Co-authored-by: 1358 <1583407+1358@users.noreply.github.com>
This commit is contained in:
Tom Mittelstädt
2026-09-01 21:48:02 +02:00
committed by GitHub
co-authored by 1358
parent 263f394d9f
commit f945b3d2ff
14 changed files with 313 additions and 28 deletions
+25 -1
View File
@@ -7,7 +7,11 @@ from typing import TYPE_CHECKING, override
import caldav
from homeassistant.components.calendar import CalendarEvent, extract_offset
from homeassistant.components.calendar import (
CalendarEvent,
CalendarEventStatus,
extract_offset,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt as dt_util
@@ -23,6 +27,24 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
OFFSET = "!!"
def _get_status(vevent: caldav.CalendarObjectResource) -> CalendarEventStatus | None:
"""Return the rfc5545 STATUS of a VEVENT, if a calendar entity reports it.
Anything outside the supported set is dropped rather than passed on, which
covers both the cancelled status a calendar entity does not report and the
iana-tokens and x-names that rfc5545 also permits here: reporting no status
at all is closer to the truth than reporting one the consumer cannot
interpret.
"""
if (value := get_attr_value(vevent, "status")) is None:
return None
try:
return CalendarEventStatus(value.lower())
except ValueError:
_LOGGER.debug("Ignoring unsupported event status %s", value)
return None
class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
"""Class to utilize the calendar dav client object to get next event."""
@@ -86,6 +108,7 @@ class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
if (v := get_attr_value(vevent, "recurrence_id")) is not None
else None
),
status=_get_status(vevent),
)
)
@@ -194,6 +217,7 @@ class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
if (v := get_attr_value(vevent, "recurrence_id")) is not None
else None
),
status=_get_status(vevent),
)
return next_event, offset
@@ -69,6 +69,7 @@ from .const import (
LIST_EVENT_FIELDS,
CalendarEntityFeature,
CalendarEntityStateAttribute,
CalendarEventStatus,
)
# mypy: disallow-any-generics
@@ -379,6 +380,7 @@ class CalendarEvent:
uid: str | None = None
recurrence_id: str | None = None
rrule: str | None = None
status: CalendarEventStatus | None = None
@property
def start_datetime_local(self) -> datetime.datetime:
@@ -33,6 +33,22 @@ class CalendarEntityFeature(IntFlag):
UPDATE_EVENT = 4
class CalendarEventStatus(StrEnum):
"""Status of a calendar event.
A subset of the statuses defined by the rfc5545 STATUS property: a calendar
entity does not return cancelled events, so that value is not represented
here.
An event without a status is not the same as a confirmed event: it means
the calendar did not report one, either because the source does not
support it or because the integration does not read it yet.
"""
CONFIRMED = "confirmed"
TENTATIVE = "tentative"
# rfc5545 fields
EVENT_UID = "uid"
EVENT_START = "dtstart"
@@ -43,6 +59,7 @@ EVENT_LOCATION = "location"
EVENT_RECURRENCE_ID = "recurrence_id"
EVENT_RECURRENCE_RANGE = "recurrence_range"
EVENT_RRULE = "rrule"
EVENT_STATUS = "status"
# Service call fields
EVENT_START_DATE = "start_date"
@@ -69,4 +86,5 @@ LIST_EVENT_FIELDS = {
EVENT_SUMMARY,
EVENT_DESCRIPTION,
EVENT_LOCATION,
EVENT_STATUS,
}
@@ -32,6 +32,7 @@ from homeassistant.components.calendar import (
CalendarEntityDescription,
CalendarEntityFeature,
CalendarEvent,
CalendarEventStatus,
extract_offset,
is_offset_reached,
)
@@ -535,6 +536,11 @@ def _get_calendar_event(event: Event) -> CalendarEvent:
end=event.end.value,
description=event.description,
location=event.location,
# The Google API defaults an omitted status to confirmed, and gcal_sync
# applies that default, so this is never None. It drops cancelled
# events when building the timeline, so only the statuses a calendar
# entity reports reach here, already in lower case.
status=CalendarEventStatus(event.status.value),
)
@@ -21,6 +21,7 @@ from homeassistant.components.calendar import (
CalendarEntity,
CalendarEntityFeature,
CalendarEvent,
CalendarEventStatus,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -228,6 +229,23 @@ def _parse_event(event: dict[str, Any]) -> Event:
raise vol.Invalid("Error parsing event input fields") from err
def _get_status(event: Event) -> CalendarEventStatus | None:
"""Return the status of an event, if a calendar entity reports that status.
ical models the full rfc5545 set, which includes cancelled, and an imported
calendar can contain such an event. A calendar entity does not report a
cancelled status, so anything outside the supported set maps to no status.
ical's enum is a plain (str, Enum) rather than a StrEnum, so its value has
to be read explicitly.
"""
if event.status is None:
return None
try:
return CalendarEventStatus(event.status.value.lower())
except ValueError:
return None
def _get_calendar_event(event: Event) -> CalendarEvent:
"""Return a CalendarEvent from an API event."""
start: datetime | date
@@ -252,4 +270,5 @@ def _get_calendar_event(event: Event) -> CalendarEvent:
rrule=event.rrule.as_rrule_str() if event.rrule else None,
recurrence_id=event.recurrence_id,
location=event.location,
status=_get_status(event),
)
+82 -27
View File
@@ -381,6 +381,44 @@ def _mock_calendar(name: str, supported_components: list[str] | None = None) ->
return calendar
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.
Used by tests that assert on how one specific VEVENT property is parsed,
which the shared EVENTS series cannot express: it is fixed at 18 entries
that other tests count on.
"""
calendar = Mock()
calendar.name = "Example"
calendar.get_supported_components = MagicMock(return_value=["VEVENT"])
calendar.search = MagicMock(
return_value=[Event(None, "0.ics", vevent, calendar, uid)]
)
with patch(
"homeassistant.components.caldav.calendar.caldav.DAVClient"
) as mock_client:
mock_client.return_value.principal.return_value.calendars.return_value = [
calendar
]
assert await async_setup_component(
hass, "calendar", {"calendar": CALDAV_CONFIG}
)
await hass.async_block_till_done()
client = await hass_client()
response = await client.get(
f"/api/calendars/{TEST_ENTITY}?start=2017-11-27&end=2017-11-28"
)
assert response.status == HTTPStatus.OK
return await response.json()
@pytest.fixture(name="config")
def mock_config() -> dict[str, Any]:
"""Fixture to provide calendar configuration.yaml."""
@@ -1078,6 +1116,7 @@ async def test_get_events_custom_calendars(
"uid": "0",
"recurrence_id": None,
"rrule": None,
"status": None,
}
]
@@ -1101,41 +1140,57 @@ LOCATION:Hamburg
DESCRIPTION:This occurrence was moved
END:VEVENT
END:VCALENDAR"""
calendar = Mock()
calendar.name = "Example"
calendar.get_supported_components = MagicMock(return_value=["VEVENT"])
calendar.search = MagicMock(
return_value=[
Event(
None, "0.ics", vevent_with_recurrence_id, calendar, "original-event-uid"
)
]
events = await _get_api_events_for_vevent(
hass, hass_client, vevent_with_recurrence_id, "original-event-uid"
)
with patch(
"homeassistant.components.caldav.calendar.caldav.DAVClient"
) as mock_client:
mock_client.return_value.principal.return_value.calendars.return_value = [
calendar
]
assert await async_setup_component(
hass, "calendar", {"calendar": CALDAV_CONFIG}
)
await hass.async_block_till_done()
client = await hass_client()
response = await client.get(
f"/api/calendars/{TEST_ENTITY}?start=2017-11-27&end=2017-11-28"
)
assert response.status == HTTPStatus.OK
events = await response.json()
assert len(events) == 1
assert events[0]["uid"] == "original-event-uid"
assert events[0]["recurrence_id"] == "2017-11-27 17:00:00+00:00"
assert events[0]["summary"] == "Modified occurrence"
ICS_WITH_STATUS = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//E-Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:status-event-uid
DTSTAMP:20171125T000000Z
DTSTART:20171127T170000Z
DTEND:20171127T180000Z
SUMMARY:This is an event with a status
LOCATION:Hamburg
DESCRIPTION:Surprisingly rainy
STATUS:{status}
END:VEVENT
END:VCALENDAR"""
@pytest.mark.parametrize(
("status", "expected_status"),
[
pytest.param("TENTATIVE", "tentative", id="tentative"),
pytest.param("CONFIRMED", "confirmed", id="confirmed"),
pytest.param("Tentative", "tentative", id="mixed_case"),
pytest.param("CANCELLED", None, id="cancelled_is_not_reported"),
pytest.param("X-VENDOR-SPECIFIC", None, id="unsupported_value"),
],
)
async def test_get_events_with_status(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
status: str,
expected_status: str | None,
) -> None:
"""Test that the rfc5545 STATUS property is populated from VEVENT data."""
events = await _get_api_events_for_vevent(
hass, hass_client, ICS_WITH_STATUS.format(status=status), "status-event-uid"
)
assert len(events) == 1
assert events[0]["status"] == expected_status
@pytest.mark.parametrize(
("calendars"),
[
+38
View File
@@ -1556,6 +1556,7 @@ async def test_working_location_get_events(
"summary": "Home",
"description": "test event",
"location": "Test Cases",
"status": "confirmed",
},
{
"start": "2026-08-25",
@@ -1563,6 +1564,7 @@ async def test_working_location_get_events(
"summary": "Office",
"description": "test event",
"location": "Test Cases",
"status": "confirmed",
},
{
"start": "2026-08-31",
@@ -1570,6 +1572,7 @@ async def test_working_location_get_events(
"summary": "Home",
"description": "test event",
"location": "Test Cases",
"status": "confirmed",
},
]
}
@@ -1652,6 +1655,7 @@ async def test_working_location_ignore_availability_false(
"summary": "Home",
"description": "test event",
"location": "Test Cases",
"status": "confirmed",
}
]
}
@@ -1778,3 +1782,37 @@ async def test_calendar_background_color(
entity = entity_registry.async_get("calendar.test_calendar")
assert entity is not None
assert entity.options.get("calendar", {}).get("color") == expected_color
@pytest.mark.freeze_time("2022-03-27 12:05:00+00:00")
@pytest.mark.parametrize(
("event_status", "expected_status"),
[
pytest.param({"status": "tentative"}, "tentative", id="tentative"),
pytest.param({"status": "confirmed"}, "confirmed", id="confirmed"),
# The Google API documents confirmed as the default for an omitted
# status and gcal_sync applies it, so it is never reported as unset.
pytest.param({}, "confirmed", id="defaults_to_confirmed"),
],
# Cancelled is not covered: in the Google API it means deleted rather than
# called off, and gcal_sync drops those events when building the timeline,
# so they never reach the integration.
)
async def test_http_api_event_status(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_events_list_items: ApiResult,
component_setup: ComponentSetup,
event_status: dict[str, str],
expected_status: str,
) -> None:
"""Test that the event status is returned by the API."""
mock_events_list_items([{**TEST_EVENT, **upcoming(), **event_status}])
assert await component_setup()
client = await hass_client()
response = await client.get(upcoming_event_url())
assert response.status == HTTPStatus.OK
events = await response.json()
assert len(events) == 1
assert events[0]["status"] == expected_status
@@ -28,6 +28,7 @@
'start': dict({
'date': '2024-09-21',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
}),
@@ -42,6 +43,7 @@
'start': dict({
'date': '2024-09-21',
}),
'status': None,
'summary': 'Fitnessstudio besuchen',
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
}),
@@ -56,6 +58,7 @@
'start': dict({
'date': '2024-09-22',
}),
'status': None,
'summary': 'Zahnseide benutzen',
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
}),
@@ -70,6 +73,7 @@
'start': dict({
'date': '2024-09-22',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
}),
@@ -84,6 +88,7 @@
'start': dict({
'date': '2024-09-22',
}),
'status': None,
'summary': 'Fitnessstudio besuchen',
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
}),
@@ -98,6 +103,7 @@
'start': dict({
'date': '2024-09-22',
}),
'status': None,
'summary': 'Arbeite an einem kreativen Projekt',
'uid': '6e53f1f5-a315-4edd-984d-8d762e4a08ef',
}),
@@ -112,6 +118,7 @@
'start': dict({
'date': '2024-09-23',
}),
'status': None,
'summary': 'Zahnseide benutzen',
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
}),
@@ -126,6 +133,7 @@
'start': dict({
'date': '2024-09-23',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
}),
@@ -140,6 +148,7 @@
'start': dict({
'date': '2024-09-24',
}),
'status': None,
'summary': 'Zahnseide benutzen',
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
}),
@@ -154,6 +163,7 @@
'start': dict({
'date': '2024-09-24',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
}),
@@ -168,6 +178,7 @@
'start': dict({
'date': '2024-09-25',
}),
'status': None,
'summary': 'Zahnseide benutzen',
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
}),
@@ -182,6 +193,7 @@
'start': dict({
'date': '2024-09-25',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
}),
@@ -196,6 +208,7 @@
'start': dict({
'date': '2024-09-25',
}),
'status': None,
'summary': 'Fitnessstudio besuchen',
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
}),
@@ -210,6 +223,7 @@
'start': dict({
'date': '2024-09-26',
}),
'status': None,
'summary': 'Zahnseide benutzen',
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
}),
@@ -224,6 +238,7 @@
'start': dict({
'date': '2024-09-26',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
}),
@@ -238,6 +253,7 @@
'start': dict({
'date': '2024-09-27',
}),
'status': None,
'summary': 'Zahnseide benutzen',
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
}),
@@ -252,6 +268,7 @@
'start': dict({
'date': '2024-09-27',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
}),
@@ -266,6 +283,7 @@
'start': dict({
'date': '2024-09-28',
}),
'status': None,
'summary': 'Zahnseide benutzen',
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
}),
@@ -280,6 +298,7 @@
'start': dict({
'date': '2024-09-28',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
}),
@@ -294,6 +313,7 @@
'start': dict({
'date': '2024-09-28',
}),
'status': None,
'summary': 'Fitnessstudio besuchen',
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
}),
@@ -308,6 +328,7 @@
'start': dict({
'date': '2024-09-29',
}),
'status': None,
'summary': 'Zahnseide benutzen',
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
}),
@@ -322,6 +343,7 @@
'start': dict({
'date': '2024-09-29',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
}),
@@ -336,6 +358,7 @@
'start': dict({
'date': '2024-09-29',
}),
'status': None,
'summary': 'Fitnessstudio besuchen',
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
}),
@@ -350,6 +373,7 @@
'start': dict({
'date': '2024-09-30',
}),
'status': None,
'summary': 'Zahnseide benutzen',
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
}),
@@ -364,6 +388,7 @@
'start': dict({
'date': '2024-09-30',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
}),
@@ -378,6 +403,7 @@
'start': dict({
'date': '2024-10-01',
}),
'status': None,
'summary': 'Zahnseide benutzen',
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
}),
@@ -392,6 +418,7 @@
'start': dict({
'date': '2024-10-01',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
}),
@@ -406,6 +433,7 @@
'start': dict({
'date': '2024-10-02',
}),
'status': None,
'summary': 'Zahnseide benutzen',
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
}),
@@ -420,6 +448,7 @@
'start': dict({
'date': '2024-10-02',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
}),
@@ -434,6 +463,7 @@
'start': dict({
'date': '2024-10-02',
}),
'status': None,
'summary': 'Fitnessstudio besuchen',
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
}),
@@ -448,6 +478,7 @@
'start': dict({
'date': '2024-10-03',
}),
'status': None,
'summary': 'Zahnseide benutzen',
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
}),
@@ -462,6 +493,7 @@
'start': dict({
'date': '2024-10-03',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
}),
@@ -476,6 +508,7 @@
'start': dict({
'date': '2024-10-04',
}),
'status': None,
'summary': 'Zahnseide benutzen',
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
}),
@@ -490,6 +523,7 @@
'start': dict({
'date': '2024-10-04',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
}),
@@ -504,6 +538,7 @@
'start': dict({
'date': '2024-10-05',
}),
'status': None,
'summary': 'Zahnseide benutzen',
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
}),
@@ -518,6 +553,7 @@
'start': dict({
'date': '2024-10-05',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
}),
@@ -532,6 +568,7 @@
'start': dict({
'date': '2024-10-05',
}),
'status': None,
'summary': 'Fitnessstudio besuchen',
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
}),
@@ -546,6 +583,7 @@
'start': dict({
'date': '2024-10-06',
}),
'status': None,
'summary': 'Zahnseide benutzen',
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
}),
@@ -560,6 +598,7 @@
'start': dict({
'date': '2024-10-06',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
}),
@@ -574,6 +613,7 @@
'start': dict({
'date': '2024-10-06',
}),
'status': None,
'summary': 'Fitnessstudio besuchen',
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
}),
@@ -588,6 +628,7 @@
'start': dict({
'date': '2024-10-06',
}),
'status': None,
'summary': 'Monatliche Finanzübersicht erstellen',
'uid': '369afeed-61e3-4bf7-9747-66e05807134c',
}),
@@ -602,6 +643,7 @@
'start': dict({
'date': '2024-10-07',
}),
'status': None,
'summary': 'Zahnseide benutzen',
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
}),
@@ -616,6 +658,7 @@
'start': dict({
'date': '2024-10-07',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
}),
@@ -634,6 +677,7 @@
'start': dict({
'dateTime': '2024-09-21T20:00:00+02:00',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
@@ -648,6 +692,7 @@
'start': dict({
'dateTime': '2024-09-22T20:00:00+02:00',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
@@ -662,6 +707,7 @@
'start': dict({
'dateTime': '2024-09-23T20:00:00+02:00',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
@@ -676,6 +722,7 @@
'start': dict({
'dateTime': '2024-09-24T20:00:00+02:00',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
@@ -690,6 +737,7 @@
'start': dict({
'dateTime': '2024-09-25T20:00:00+02:00',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
@@ -704,6 +752,7 @@
'start': dict({
'dateTime': '2024-09-26T20:00:00+02:00',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
@@ -718,6 +767,7 @@
'start': dict({
'dateTime': '2024-09-27T20:00:00+02:00',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
@@ -732,6 +782,7 @@
'start': dict({
'dateTime': '2024-09-28T20:00:00+02:00',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
@@ -746,6 +797,7 @@
'start': dict({
'dateTime': '2024-09-29T20:00:00+02:00',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
@@ -760,6 +812,7 @@
'start': dict({
'dateTime': '2024-09-30T20:00:00+02:00',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
@@ -774,6 +827,7 @@
'start': dict({
'dateTime': '2024-10-01T20:00:00+02:00',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
@@ -788,6 +842,7 @@
'start': dict({
'dateTime': '2024-10-02T20:00:00+02:00',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
@@ -802,6 +857,7 @@
'start': dict({
'dateTime': '2024-10-03T20:00:00+02:00',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
@@ -816,6 +872,7 @@
'start': dict({
'dateTime': '2024-10-04T20:00:00+02:00',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
@@ -830,6 +887,7 @@
'start': dict({
'dateTime': '2024-10-05T20:00:00+02:00',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
@@ -844,6 +902,7 @@
'start': dict({
'dateTime': '2024-10-06T20:00:00+02:00',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
@@ -858,6 +917,7 @@
'start': dict({
'dateTime': '2024-10-07T20:00:00+02:00',
}),
'status': None,
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
@@ -876,6 +936,7 @@
'start': dict({
'dateTime': '2024-09-22T02:00:00+02:00',
}),
'status': None,
'summary': 'Rechnungen bezahlen',
'uid': '2f6fcabc-f670-4ec3-ba65-817e8deea490_91c09432-10ac-4a49-bd20-823081ec29ed',
}),
@@ -894,6 +955,7 @@
'start': dict({
'date': '2024-08-31',
}),
'status': None,
'summary': 'Rechnungen bezahlen',
'uid': '2f6fcabc-f670-4ec3-ba65-817e8deea490',
}),
@@ -908,6 +970,7 @@
'start': dict({
'date': '2024-09-21',
}),
'status': None,
'summary': 'Wochenendausflug planen',
'uid': '86ea2475-d1b5-4020-bdcc-c188c7996afa',
}),
@@ -922,6 +985,7 @@
'start': dict({
'date': '2024-09-27',
}),
'status': None,
'summary': 'Buch zu Ende lesen',
'uid': '88de7cd9-af2b-49ce-9afd-bf941d87336b',
}),
@@ -1216,3 +1216,41 @@ async def test_adjacent_events_stay_on(
state = hass.states.get(TEST_ENTITY)
assert state.state == STATE_ON
assert state.attributes["message"] == "Second"
ICS_WITH_STATUS = """BEGIN:VCALENDAR
BEGIN:VEVENT
SUMMARY:Bastille Day Party
DTSTART:19970714
DTEND:19970715
STATUS:{status}
END:VEVENT
END:VCALENDAR
"""
@pytest.mark.parametrize(
("ics_content", "expected_status"),
[
pytest.param(
ICS_WITH_STATUS.format(status="TENTATIVE"), "tentative", id="tentative"
),
pytest.param(
ICS_WITH_STATUS.format(status="CONFIRMED"), "confirmed", id="confirmed"
),
pytest.param(
ICS_WITH_STATUS.format(status="CANCELLED"),
None,
id="cancelled_is_not_reported",
),
],
)
@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."""
events = await get_events("1997-07-13T00:00:00", "1997-07-16T00:00:00")
assert len(events) == 1
assert events[0]["status"] == expected_status
@@ -44,6 +44,7 @@
'start': dict({
'date': '2024-01-22',
}),
'status': None,
'summary': 'Zoete aardappel curry traybake',
'uid': None,
}),
@@ -58,6 +59,7 @@
'start': dict({
'date': '2024-01-23',
}),
'status': None,
'summary': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο (1)',
'uid': None,
}),
@@ -72,6 +74,7 @@
'start': dict({
'date': '2024-01-23',
}),
'status': None,
'summary': 'Pampered Chef Double Chocolate Mocha Trifle',
'uid': None,
}),
@@ -86,6 +89,7 @@
'start': dict({
'date': '2024-01-22',
}),
'status': None,
'summary': 'Cheeseburger Sliders (Easy, 30-min Recipe)',
'uid': None,
}),
@@ -100,6 +104,7 @@
'start': dict({
'date': '2024-01-23',
}),
'status': None,
'summary': 'All-American Beef Stew Recipe',
'uid': None,
}),
@@ -114,6 +119,7 @@
'start': dict({
'date': '2024-01-23',
}),
'status': None,
'summary': 'Miso Udon Noodles with Spinach and Tofu',
'uid': None,
}),
@@ -128,6 +134,7 @@
'start': dict({
'date': '2024-01-21',
}),
'status': None,
'summary': 'Aquavite',
'uid': None,
}),
@@ -12,6 +12,7 @@
'start': dict({
'dateTime': '2024-04-26T14:00:00-06:00',
}),
'status': None,
'summary': 'Uffe',
'uid': '040000008200E00074C5B7101A82E00800000000687C546B5596DA01000000000000000010000000309AE93C8C3A94489F90ADBEA30C2F2B',
}),
@@ -78,6 +78,7 @@ def get_events_response(start: dict[str, str], end: dict[str, str]) -> dict[str,
"uid": None,
"recurrence_id": None,
"rrule": None,
"status": None,
}
@@ -20,6 +20,7 @@
'start': dict({
'date': '2022-01-06',
}),
'status': None,
'summary': 'Christmas tree pickup',
'uid': None,
}),
@@ -20,6 +20,7 @@
'start': dict({
'dateTime': '2023-08-29T12:06:51-07:00',
}),
'status': None,
'summary': 'Walk',
'uid': None,
}),
@@ -34,6 +35,7 @@
'start': dict({
'dateTime': '2023-08-31T01:08:27-07:00',
}),
'status': None,
'summary': 'Walk',
'uid': None,
}),
@@ -48,6 +50,7 @@
'start': dict({
'dateTime': '2023-08-04T09:00:39-07:00',
}),
'status': None,
'summary': 'Walk',
'uid': None,
}),
@@ -62,6 +65,7 @@
'start': dict({
'dateTime': '2023-09-22T16:33:55-07:00',
}),
'status': None,
'summary': 'Walk',
'uid': None,
}),
@@ -76,6 +80,7 @@
'start': dict({
'dateTime': '2023-09-14T11:20:49-07:00',
}),
'status': None,
'summary': 'Walk',
'uid': None,
}),
@@ -90,6 +95,7 @@
'start': dict({
'dateTime': '2023-09-22T16:55:53-07:00',
}),
'status': None,
'summary': 'Walk',
'uid': None,
}),
@@ -104,6 +110,7 @@
'start': dict({
'dateTime': '2023-09-14T10:42:31-07:00',
}),
'status': None,
'summary': 'Walk',
'uid': None,
}),
@@ -118,6 +125,7 @@
'start': dict({
'dateTime': '2023-10-09T00:12:49-07:00',
}),
'status': None,
'summary': 'Walk',
'uid': None,
}),
@@ -132,6 +140,7 @@
'start': dict({
'dateTime': '2023-10-09T02:39:43-07:00',
}),
'status': None,
'summary': 'Walk',
'uid': None,
}),
@@ -146,6 +155,7 @@
'start': dict({
'dateTime': '2023-10-09T02:13:23-07:00',
}),
'status': None,
'summary': 'Walk',
'uid': None,
}),
@@ -160,6 +170,7 @@
'start': dict({
'dateTime': '2023-10-09T02:13:23-07:00',
}),
'status': None,
'summary': 'Walk',
'uid': None,
}),