Cleanup calendar APIs and introduce a dataclass for representing events (#68843)

* Introduce data class to hold calendar event data

* Rename CalendarEventDevice to CalendarEntity

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Fix docstring on google calendar api conversion function.

* Update todoist to new calendar enttiy api, tested manually

* Add back old API for a legacy compatibility layer

* Add deprecation warning for old calendar APIs

* Fix deprecation warning

* Fix merge for missing summary #69520

* Add mypy typing for newly introduced classes

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
pull/69826/head
Allen Porter 2022-04-10 12:04:07 -07:00 committed by GitHub
parent c98d120ba0
commit f99b6004ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 341 additions and 181 deletions

View File

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

View File

@ -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,11 +309,31 @@ 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)
# 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(
[
{
"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):
"""View to retrieve calendar list."""

View File

@ -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]

View File

@ -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"),
)

View File

@ -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)

View File

@ -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()

View File

@ -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",
}
]

View File

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

View File

@ -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",