diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 1955751d4fd..c5b2b3a790a 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -12,9 +12,9 @@ import voluptuous as vol from homeassistant.components.calendar import ( ENTITY_ID_FORMAT, PLATFORM_SCHEMA, - CalendarEventDevice, + CalendarEntity, + CalendarEvent, extract_offset, - get_date, is_offset_reached, ) from homeassistant.const import ( @@ -104,7 +104,7 @@ def setup_platform( device_id = f"{cust_calendar[CONF_CALENDAR]} {cust_calendar[CONF_NAME]}" entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass) calendar_devices.append( - WebDavCalendarEventDevice( + WebDavCalendarEntity( name, calendar, entity_id, days, True, cust_calendar[CONF_SEARCH] ) ) @@ -115,24 +115,24 @@ def setup_platform( device_id = calendar.name entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass) calendar_devices.append( - WebDavCalendarEventDevice(name, calendar, entity_id, days) + WebDavCalendarEntity(name, calendar, entity_id, days) ) add_entities(calendar_devices, True) -class WebDavCalendarEventDevice(CalendarEventDevice): +class WebDavCalendarEntity(CalendarEntity): """A device for getting the next Task from a WebDav Calendar.""" def __init__(self, name, calendar, entity_id, days, all_day=False, search=None): """Create the WebDav Calendar Event Device.""" self.data = WebDavCalendarData(calendar, days, all_day, search) self.entity_id = entity_id - self._event = None + self._event: CalendarEvent | None = None self._attr_name = name @property - def event(self): + def event(self) -> CalendarEvent | None: """Return the next upcoming event.""" return self._event @@ -147,11 +147,11 @@ class WebDavCalendarEventDevice(CalendarEventDevice): if event is None: self._event = event return - (summary, offset) = extract_offset(event["summary"], OFFSET) - event["summary"] = summary + (summary, offset) = extract_offset(event.summary, OFFSET) + event.summary = summary self._event = event self._attr_extra_state_attributes = { - "offset_reached": is_offset_reached(get_date(event["start"]), offset) + "offset_reached": is_offset_reached(event.start_datetime_local, offset) } @@ -166,7 +166,9 @@ class WebDavCalendarData: self.search = search self.event = None - async def async_get_events(self, hass, start_date, end_date): + async def async_get_events( + self, hass: HomeAssistant, start_date: datetime, end_date: datetime + ) -> list[CalendarEvent]: """Get all events in a specific time frame.""" # Get event list from the current calendar vevent_list = await hass.async_add_executor_job( @@ -180,22 +182,15 @@ class WebDavCalendarData: vevent = event.instance.vevent if not self.is_matching(vevent, self.search): continue - uid = None - if hasattr(vevent, "uid"): - uid = vevent.uid.value - data = { - "uid": uid, - "summary": vevent.summary.value, - "start": self.get_hass_date(vevent.dtstart.value), - "end": self.get_hass_date(self.get_end_date(vevent)), - "location": self.get_attr_value(vevent, "location"), - "description": self.get_attr_value(vevent, "description"), - } - - data["start"] = get_date(data["start"]).isoformat() - data["end"] = get_date(data["end"]).isoformat() - - event_list.append(data) + event_list.append( + CalendarEvent( + summary=vevent.summary.value, + start=vevent.dtstart.value, + end=self.get_end_date(vevent), + location=self.get_attr_value(vevent, "location"), + description=self.get_attr_value(vevent, "description"), + ) + ) return event_list @@ -269,13 +264,13 @@ class WebDavCalendarData: return # Populate the entity attributes with the event values - self.event = { - "summary": vevent.summary.value, - "start": self.get_hass_date(vevent.dtstart.value), - "end": self.get_hass_date(self.get_end_date(vevent)), - "location": self.get_attr_value(vevent, "location"), - "description": self.get_attr_value(vevent, "description"), - } + self.event = CalendarEvent( + summary=vevent.summary.value, + start=vevent.dtstart.value, + end=self.get_end_date(vevent), + location=self.get_attr_value(vevent, "location"), + description=self.get_attr_value(vevent, "description"), + ) @staticmethod def is_matching(vevent, search): @@ -305,14 +300,6 @@ class WebDavCalendarData: WebDavCalendarData.get_end_date(vevent) ) - @staticmethod - def get_hass_date(obj): - """Return if the event matches.""" - if isinstance(obj, datetime): - return {"dateTime": obj.isoformat()} - - return {"date": obj.isoformat()} - @staticmethod def to_datetime(obj): """Return a datetime.""" diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index d58903e8b0c..cb4af1ab671 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -1,6 +1,7 @@ """Support for Google Calendar event device sensors.""" from __future__ import annotations +from dataclasses import dataclass import datetime from http import HTTPStatus import logging @@ -73,6 +74,48 @@ def get_date(date: dict[str, Any]) -> datetime.datetime: return dt.as_local(parsed_datetime) +@dataclass +class CalendarEvent: + """An event on a calendar.""" + + start: datetime.date | datetime.datetime + end: datetime.date | datetime.datetime + summary: str + description: str | None = None + location: str | None = None + + @property + def start_datetime_local(self) -> datetime.datetime: + """Return event start time as a local datetime.""" + return _get_datetime_local(self.start) + + @property + def end_datetime_local(self) -> datetime.datetime: + """Return event end time as a local datetime.""" + return _get_datetime_local(self.end) + + @property + def all_day(self) -> bool: + """Return true if the event is an all day event.""" + return not isinstance(self.start, datetime.datetime) + + +def _get_datetime_local( + dt_or_d: datetime.datetime | datetime.date, +) -> datetime.datetime: + """Convert a calendar event date/datetime to a datetime if needed.""" + if isinstance(dt_or_d, datetime.datetime): + return dt.as_local(dt_or_d) + return dt.start_of_local_day(dt_or_d) + + +def _get_api_date(dt_or_d: datetime.datetime | datetime.date) -> dict[str, str]: + """Convert a calendar event date/datetime to a datetime if needed.""" + if isinstance(dt_or_d, datetime.datetime): + return {"dateTime": dt.as_local(dt_or_d).isoformat()} + return {"date": dt_or_d.isoformat()} + + def normalize_event(event: dict[str, Any]) -> dict[str, Any]: """Normalize a calendar event.""" normalized_event: dict[str, Any] = {} @@ -132,7 +175,15 @@ def is_offset_reached( class CalendarEventDevice(Entity): - """Base class for calendar event entities.""" + """Legacy API for calendar event entities.""" + + def __init_subclass__(cls, **kwargs: Any) -> None: + """Print deprecation warning.""" + super().__init_subclass__(**kwargs) + _LOGGER.warning( + "CalendarEventDevice is deprecated, modify %s to extend CalendarEntity", + cls.__name__, + ) @property def event(self) -> dict[str, Any] | None: @@ -143,6 +194,7 @@ class CalendarEventDevice(Entity): @property def state_attributes(self) -> dict[str, Any] | None: """Return the entity state attributes.""" + if (event := self.event) is None: return None @@ -186,6 +238,53 @@ class CalendarEventDevice(Entity): raise NotImplementedError() +class CalendarEntity(Entity): + """Base class for calendar event entities.""" + + @property + def event(self) -> CalendarEvent | None: + """Return the next upcoming event.""" + raise NotImplementedError() + + @final + @property + def state_attributes(self) -> dict[str, Any] | None: + """Return the entity state attributes.""" + if (event := self.event) is None: + return None + + return { + "message": event.summary, + "all_day": event.all_day, + "start_time": event.start_datetime_local.strftime(DATE_STR_FORMAT), + "end_time": event.end_datetime_local.strftime(DATE_STR_FORMAT), + "location": event.location if event.location else "", + "description": event.description if event.description else "", + } + + @property + def state(self) -> str | None: + """Return the state of the calendar event.""" + if (event := self.event) is None: + return STATE_OFF + + now = dt.now() + + if event.start_datetime_local <= now < event.end_datetime_local: + return STATE_ON + + return STATE_OFF + + async def async_get_events( + self, + hass: HomeAssistant, + start_date: datetime.datetime, + end_date: datetime.datetime, + ) -> list[CalendarEvent]: + """Return calendar events within a datetime range.""" + raise NotImplementedError() + + class CalendarEventView(http.HomeAssistantView): """View to retrieve calendar content.""" @@ -203,7 +302,6 @@ class CalendarEventView(http.HomeAssistantView): end = request.query.get("end") if start is None or end is None or entity is None: return web.Response(status=HTTPStatus.BAD_REQUEST) - assert isinstance(entity, CalendarEventDevice) try: start_date = dt.parse_datetime(start) end_date = dt.parse_datetime(end) @@ -211,10 +309,30 @@ class CalendarEventView(http.HomeAssistantView): return web.Response(status=HTTPStatus.BAD_REQUEST) if start_date is None or end_date is None: return web.Response(status=HTTPStatus.BAD_REQUEST) - event_list = await entity.async_get_events( + + # Compatibility shim for old API + if isinstance(entity, CalendarEventDevice): + event_list = await entity.async_get_events( + request.app["hass"], start_date, end_date + ) + return self.json(event_list) + + if not isinstance(entity, CalendarEntity): + return web.Response(status=HTTPStatus.BAD_REQUEST) + + calendar_event_list = await entity.async_get_events( request.app["hass"], start_date, end_date ) - return self.json(event_list) + return self.json( + [ + { + "summary": event.summary, + "start": _get_api_date(event.start), + "end": _get_api_date(event.end), + } + for event in calendar_event_list + ] + ) class CalendarListView(http.HomeAssistantView): diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index da0badd87b1..42ec04e42f2 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -2,8 +2,14 @@ from __future__ import annotations import copy +import datetime -from homeassistant.components.calendar import CalendarEventDevice, get_date +from homeassistant.components.calendar import ( + CalendarEntity, + CalendarEvent, + CalendarEventDevice, + get_date, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -17,37 +23,66 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Demo Calendar platform.""" - calendar_data_future = DemoGoogleCalendarDataFuture() - calendar_data_current = DemoGoogleCalendarDataCurrent() add_entities( [ - DemoGoogleCalendar(hass, calendar_data_future, "Calendar 1"), - DemoGoogleCalendar(hass, calendar_data_current, "Calendar 2"), + DemoCalendar(calendar_data_future(), "Calendar 1"), + DemoCalendar(calendar_data_current(), "Calendar 2"), + LegacyDemoCalendar("Calendar 3"), ] ) -class DemoGoogleCalendarData: +def calendar_data_future() -> CalendarEvent: + """Representation of a Demo Calendar for a future event.""" + one_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30) + return CalendarEvent( + start=one_hour_from_now, + end=one_hour_from_now + datetime.timedelta(minutes=60), + summary="Future Event", + ) + + +def calendar_data_current() -> CalendarEvent: + """Representation of a Demo Calendar for a current event.""" + middle_of_event = dt_util.now() - datetime.timedelta(minutes=30) + return CalendarEvent( + start=middle_of_event, + end=middle_of_event + datetime.timedelta(minutes=60), + summary="Current Event", + ) + + +class DemoCalendar(CalendarEntity): """Representation of a Demo Calendar element.""" - event = None + def __init__(self, event: CalendarEvent, name: str) -> None: + """Initialize demo calendar.""" + self._event = event + self._name = name - async def async_get_events(self, hass, start_date, end_date): - """Get all events in a specific time frame.""" - event = copy.copy(self.event) - event["title"] = event["summary"] - event["start"] = get_date(event["start"]).isoformat() - event["end"] = get_date(event["end"]).isoformat() - return [event] + @property + def event(self) -> CalendarEvent: + """Return the next upcoming event.""" + return self._event + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + async def async_get_events(self, hass, start_date, end_date) -> list[CalendarEvent]: + """Return calendar events within a datetime range.""" + return [self._event] -class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData): - """Representation of a Demo Calendar for a future event.""" +class LegacyDemoCalendar(CalendarEventDevice): + """Calendar for exercising shim API.""" - def __init__(self): - """Set the event to a future event.""" + def __init__(self, name): + """Initialize demo calendar.""" + self._name = name one_hour_from_now = dt_util.now() + dt_util.dt.timedelta(minutes=30) - self.event = { + self._event = { "start": {"dateTime": one_hour_from_now.isoformat()}, "end": { "dateTime": ( @@ -57,36 +92,10 @@ class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData): "summary": "Future Event", } - -class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData): - """Representation of a Demo Calendar for a current event.""" - - def __init__(self): - """Set the event data.""" - middle_of_event = dt_util.now() - dt_util.dt.timedelta(minutes=30) - self.event = { - "start": {"dateTime": middle_of_event.isoformat()}, - "end": { - "dateTime": ( - middle_of_event + dt_util.dt.timedelta(minutes=60) - ).isoformat() - }, - "summary": "Current Event", - } - - -class DemoGoogleCalendar(CalendarEventDevice): - """Representation of a Demo Calendar element.""" - - def __init__(self, hass, calendar_data, name): - """Initialize demo calendar.""" - self.data = calendar_data - self._name = name - @property def event(self): """Return the next upcoming event.""" - return self.data.event + return self._event @property def name(self): @@ -94,5 +103,9 @@ class DemoGoogleCalendar(CalendarEventDevice): return self._name async def async_get_events(self, hass, start_date, end_date): - """Return calendar events within a datetime range.""" - return await self.data.async_get_events(hass, start_date, end_date) + """Get all events in a specific time frame.""" + event = copy.copy(self.event) + event["title"] = event["summary"] + event["start"] = get_date(event["start"]).isoformat() + event["end"] = get_date(event["end"]).isoformat() + return [event] diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 1174868bb78..b52f120e65a 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -2,7 +2,7 @@ from __future__ import annotations import copy -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta import logging from typing import Any @@ -10,9 +10,9 @@ from httplib2 import ServerNotFoundError from homeassistant.components.calendar import ( ENTITY_ID_FORMAT, - CalendarEventDevice, + CalendarEntity, + CalendarEvent, extract_offset, - get_date, is_offset_reached, ) from homeassistant.config_entries import ConfigEntry @@ -22,7 +22,7 @@ from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import Throttle +from homeassistant.util import Throttle, dt from . import ( CONF_CAL_ID, @@ -94,7 +94,7 @@ def _async_setup_entities( entity_id = generate_entity_id( ENTITY_ID_FORMAT, data[CONF_DEVICE_ID], hass=hass ) - entity = GoogleCalendarEventDevice( + entity = GoogleCalendarEntity( calendar_service, disc_info[CONF_CAL_ID], data, entity_id ) entities.append(entity) @@ -102,7 +102,7 @@ def _async_setup_entities( async_add_entities(entities, True) -class GoogleCalendarEventDevice(CalendarEventDevice): +class GoogleCalendarEntity(CalendarEntity): """A calendar event device.""" def __init__( @@ -117,7 +117,7 @@ class GoogleCalendarEventDevice(CalendarEventDevice): self._calendar_id = calendar_id self._search: str | None = data.get(CONF_SEARCH) self._ignore_availability: bool = data.get(CONF_IGNORE_AVAILABILITY, False) - self._event: dict[str, Any] | None = None + self._event: CalendarEvent | None = None self._name: str = data[CONF_NAME] self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET) self._offset_reached = False @@ -129,7 +129,7 @@ class GoogleCalendarEventDevice(CalendarEventDevice): return {"offset_reached": self._offset_reached} @property - def event(self) -> dict[str, Any] | None: + def event(self) -> CalendarEvent | None: """Return the next upcoming event.""" return self._event @@ -146,7 +146,7 @@ class GoogleCalendarEventDevice(CalendarEventDevice): async def async_get_events( self, hass: HomeAssistant, start_date: datetime, end_date: datetime - ) -> list[dict[str, Any]]: + ) -> list[CalendarEvent]: """Get all events in a specific time frame.""" event_list: list[dict[str, Any]] = [] page_token: str | None = None @@ -166,7 +166,8 @@ class GoogleCalendarEventDevice(CalendarEventDevice): event_list.extend(filter(self._event_filter, items)) if not page_token: break - return event_list + + return [_get_calendar_event(event) for event in event_list] @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self) -> None: @@ -181,12 +182,35 @@ class GoogleCalendarEventDevice(CalendarEventDevice): # Pick the first visible event and apply offset calculations. valid_items = filter(self._event_filter, items) - self._event = copy.deepcopy(next(valid_items, None)) - if self._event: - (summary, offset) = extract_offset( - self._event.get("summary", ""), self._offset - ) - self._event["summary"] = summary + event = copy.deepcopy(next(valid_items, None)) + if event: + (summary, offset) = extract_offset(event.get("summary", ""), self._offset) + event["summary"] = summary + self._event = _get_calendar_event(event) self._offset_reached = is_offset_reached( - get_date(self._event["start"]), offset + self._event.start_datetime_local, offset ) + else: + self._event = None + + +def _get_date_or_datetime(date_dict: dict[str, str]) -> datetime | date: + """Convert a google calendar API response to a datetime or date object.""" + if "date" in date_dict: + parsed_date = dt.parse_date(date_dict["date"]) + assert parsed_date + return parsed_date + parsed_datetime = dt.parse_datetime(date_dict["dateTime"]) + assert parsed_datetime + return parsed_datetime + + +def _get_calendar_event(event: dict[str, Any]) -> CalendarEvent: + """Return a CalendarEvent from an API event.""" + return CalendarEvent( + summary=event["summary"], + start=_get_date_or_datetime(event["start"]), + end=_get_date_or_datetime(event["end"]), + description=event.get("description"), + location=event.get("location"), + ) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 4582b1de0f0..b1e15e42221 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -1,18 +1,21 @@ """Support for Todoist task management (https://todoist.com).""" from __future__ import annotations -from datetime import datetime, timedelta, timezone +from datetime import date, datetime, timedelta, timezone import logging from todoist.api import TodoistAPI import voluptuous as vol -from homeassistant.components.calendar import PLATFORM_SCHEMA, CalendarEventDevice +from homeassistant.components.calendar import ( + PLATFORM_SCHEMA, + CalendarEntity, + CalendarEvent, +) from homeassistant.const import CONF_ID, CONF_NAME, CONF_TOKEN from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt @@ -28,7 +31,6 @@ from .const import ( CONF_PROJECT_LABEL_WHITELIST, CONF_PROJECT_WHITELIST, CONTENT, - DATETIME, DESCRIPTION, DOMAIN, DUE, @@ -102,7 +104,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -SCAN_INTERVAL = timedelta(minutes=15) +SCAN_INTERVAL = timedelta(minutes=1) def setup_platform( @@ -136,7 +138,7 @@ def setup_platform( # Project is an object, not a dict! # Because of that, we convert what we need to a dict. project_data = {CONF_NAME: project[NAME], CONF_ID: project[ID]} - project_devices.append(TodoistProjectDevice(hass, project_data, labels, api)) + project_devices.append(TodoistProjectEntity(hass, project_data, labels, api)) # Cache the names so we can easily look up name->ID. project_id_lookup[project[NAME].lower()] = project[ID] @@ -166,7 +168,7 @@ def setup_platform( # Create the custom project and add it to the devices array. project_devices.append( - TodoistProjectDevice( + TodoistProjectEntity( hass, project, labels, @@ -176,7 +178,6 @@ def setup_platform( project_id_filter, ) ) - add_entities(project_devices) def handle_new_task(call: ServiceCall) -> None: @@ -271,7 +272,7 @@ def _parse_due_date(data: DueDate, timezone_offset: int) -> datetime | None: return dt.as_utc(nowtime) -class TodoistProjectDevice(CalendarEventDevice): +class TodoistProjectEntity(CalendarEntity): """A device for getting the next Task from a Todoist Project.""" def __init__( @@ -284,7 +285,7 @@ class TodoistProjectDevice(CalendarEventDevice): whitelisted_labels=None, whitelisted_projects=None, ): - """Create the Todoist Calendar Event Device.""" + """Create the Todoist Calendar Entity.""" self.data = TodoistProjectData( data, labels, @@ -297,9 +298,9 @@ class TodoistProjectDevice(CalendarEventDevice): self._name = data[CONF_NAME] @property - def event(self): + def event(self) -> CalendarEvent: """Return the next upcoming event.""" - return self.data.event + return self.data.calendar_event @property def name(self): @@ -314,7 +315,12 @@ class TodoistProjectDevice(CalendarEventDevice): task[SUMMARY] for task in self.data.all_project_tasks ] - async def async_get_events(self, hass, start_date, end_date): + async def async_get_events( + self, + hass: HomeAssistant, + start_date: datetime, + end_date: datetime, + ) -> list[CalendarEvent]: """Get all events in a specific time frame.""" return await self.data.async_get_events(hass, start_date, end_date) @@ -336,7 +342,7 @@ class TodoistProjectDevice(CalendarEventDevice): class TodoistProjectData: """ - Class used by the Task Device service object to hold all Todoist Tasks. + Class used by the Task Entity service object to hold all Todoist Tasks. This is analogous to the GoogleCalendarData found in the Google Calendar component. @@ -409,6 +415,22 @@ class TodoistProjectData: else: self._project_id_whitelist = [] + @property + def calendar_event(self) -> CalendarEvent | None: + """Return the next upcoming calendar event.""" + if not self.event: + return None + if not self.event.get(END) or self.event.get(ALL_DAY): + start = self.event[START].date() + return CalendarEvent( + summary=self.event[SUMMARY], + start=start, + end=start + timedelta(days=1), + ) + return CalendarEvent( + summary=self.event[SUMMARY], start=self.event[START], end=self.event[END] + ) + def create_todoist_task(self, data): """ Create a dictionary based on a Task passed from the Todoist API. @@ -566,7 +588,7 @@ class TodoistProjectData: if task["due"] is None: continue # @NOTE: _parse_due_date always returns the date in UTC time. - due_date = _parse_due_date( + due_date: datetime | None = _parse_due_date( task["due"], self._api.state["user"]["tz_info"]["hours"] ) if not due_date: @@ -580,20 +602,16 @@ class TodoistProjectData: ) if start_date < due_date < end_date: + due_date_value: datetime | date = due_date if due_date == midnight: # If the due date has no time data, return just the date so that it # will render correctly as an all day event on a calendar. - due_date_value = due_date.strftime("%Y-%m-%d") - else: - due_date_value = due_date.isoformat() - event = { - "uid": task["id"], - "title": task["content"], - "start": due_date_value, - "end": due_date_value, - "allDay": True, - "summary": task["content"], - } + due_date_value = due_date.date() + event = CalendarEvent( + summary=task["content"], + start=due_date_value, + end=due_date_value, + ) events.append(event) return events @@ -644,22 +662,10 @@ class TodoistProjectData: project_tasks.remove(best_task) self.all_project_tasks.append(best_task) - self.event = self.all_project_tasks[0] - - # Convert datetime to a string again - if self.event is not None: - if self.event[START] is not None: - self.event[START] = { - DATETIME: self.event[START].strftime(DATE_STR_FORMAT) - } - if self.event[END] is not None: - self.event[END] = {DATETIME: self.event[END].strftime(DATE_STR_FORMAT)} - else: - # Home Assistant gets cranky if a calendar event never ends - # Let's set our "due date" to tomorrow - self.event[END] = { - DATETIME: (datetime.utcnow() + timedelta(days=1)).strftime( - DATE_STR_FORMAT - ) - } + event = self.all_project_tasks[0] + if event is None or event[START] is None: + _LOGGER.debug("No valid event or event start for %s", self._name) + self.event = None + return + self.event = event _LOGGER.debug("Updated %s", self._name) diff --git a/homeassistant/components/twentemilieu/calendar.py b/homeassistant/components/twentemilieu/calendar.py index 0d2768e5eb2..1c7cfcbd4fc 100644 --- a/homeassistant/components/twentemilieu/calendar.py +++ b/homeassistant/components/twentemilieu/calendar.py @@ -2,9 +2,8 @@ from __future__ import annotations from datetime import datetime -from typing import Any -from homeassistant.components.calendar import CalendarEventDevice +from homeassistant.components.calendar import CalendarEntity, CalendarEvent from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback @@ -26,7 +25,7 @@ async def async_setup_entry( async_add_entities([TwenteMilieuCalendar(coordinator, entry)]) -class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEventDevice): +class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEntity): """Defines a Twente Milieu calendar.""" _attr_name = "Twente Milieu" @@ -40,26 +39,25 @@ class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEventDevice): """Initialize the Twente Milieu entity.""" super().__init__(coordinator, entry) self._attr_unique_id = str(entry.data[CONF_ID]) - self._event: dict[str, Any] | None = None + self._event: CalendarEvent | None = None @property - def event(self) -> dict[str, Any] | None: + def event(self) -> CalendarEvent | None: """Return the next upcoming event.""" return self._event async def async_get_events( self, hass: HomeAssistant, start_date: datetime, end_date: datetime - ) -> list[dict[str, Any]]: + ) -> list[CalendarEvent]: """Return calendar events within a datetime range.""" - events: list[dict[str, Any]] = [] + events: list[CalendarEvent] = [] for waste_type, waste_dates in self.coordinator.data.items(): events.extend( - { - "all_day": True, - "start": {"date": waste_date.isoformat()}, - "end": {"date": waste_date.isoformat()}, - "summary": WASTE_TYPE_TO_DESCRIPTION[waste_type], - } + CalendarEvent( + summary=WASTE_TYPE_TO_DESCRIPTION[waste_type], + start=waste_date, + end=waste_date, + ) for waste_date in waste_dates if start_date.date() <= waste_date <= end_date.date() ) @@ -86,12 +84,11 @@ class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEventDevice): self._event = None if next_waste_pickup_date is not None and next_waste_pickup_type is not None: - self._event = { - "all_day": True, - "start": {"date": next_waste_pickup_date.isoformat()}, - "end": {"date": next_waste_pickup_date.isoformat()}, - "summary": WASTE_TYPE_TO_DESCRIPTION[next_waste_pickup_type], - } + self._event = CalendarEvent( + summary=WASTE_TYPE_TO_DESCRIPTION[next_waste_pickup_type], + start=next_waste_pickup_date, + end=next_waste_pickup_date, + ) super()._handle_coordinator_update() diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index cb23381d1c6..2131eebe997 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -934,11 +934,8 @@ async def test_get_events_custom_calendars(hass, calendar, get_api_events): events = await get_api_events("calendar.private_private") assert events == [ { - "description": "Surprisingly rainy", - "end": "2017-11-27T10:00:00-08:00", - "location": "Hamburg", - "start": "2017-11-27T09:00:00-08:00", + "end": {"dateTime": "2017-11-27T10:00:00-08:00"}, + "start": {"dateTime": "2017-11-27T09:00:00-08:00"}, "summary": "This is a normal event", - "uid": "1", } ] diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 8ab210e7180..26f9320a196 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -23,7 +23,6 @@ async def test_events_http_api(hass, hass_client): assert response.status == HTTPStatus.OK events = await response.json() assert events[0]["summary"] == "Future Event" - assert events[0]["title"] == "Future Event" async def test_calendars_http_api(hass, hass_client): @@ -37,4 +36,24 @@ async def test_calendars_http_api(hass, hass_client): assert data == [ {"entity_id": "calendar.calendar_1", "name": "Calendar 1"}, {"entity_id": "calendar.calendar_2", "name": "Calendar 2"}, + {"entity_id": "calendar.calendar_3", "name": "Calendar 3"}, ] + + +async def test_events_http_api_shim(hass, hass_client): + """Test the legacy shim calendar demo view.""" + await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) + await hass.async_block_till_done() + client = await hass_client() + response = await client.get("/api/calendars/calendar.calendar_3") + assert response.status == HTTPStatus.BAD_REQUEST + start = dt_util.now() + end = start + timedelta(days=1) + response = await client.get( + "/api/calendars/calendar.calendar_1?start={}&end={}".format( + start.isoformat(), end.isoformat() + ) + ) + assert response.status == HTTPStatus.OK + events = await response.json() + assert events[0]["summary"] == "Future Event" diff --git a/tests/components/twentemilieu/test_calendar.py b/tests/components/twentemilieu/test_calendar.py index 27e3ff8ebf3..0a0f32be212 100644 --- a/tests/components/twentemilieu/test_calendar.py +++ b/tests/components/twentemilieu/test_calendar.py @@ -76,7 +76,6 @@ async def test_api_events( events = await response.json() assert len(events) == 1 assert events[0] == { - "all_day": True, "start": {"date": "2022-01-06"}, "end": {"date": "2022-01-06"}, "summary": "Christmas Tree Pickup",