mirror of
https://github.com/home-assistant/core.git
synced 2026-09-03 20:12:16 +01:00
Add event status to calendar events (#179312)
Co-authored-by: 1358 <1583407+1358@users.noreply.github.com>
This commit is contained in:
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
[
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user