mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 13:47:35 +00:00
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>
This commit is contained in:
parent
c98d120ba0
commit
f99b6004ea
@ -12,9 +12,9 @@ import voluptuous as vol
|
|||||||
from homeassistant.components.calendar import (
|
from homeassistant.components.calendar import (
|
||||||
ENTITY_ID_FORMAT,
|
ENTITY_ID_FORMAT,
|
||||||
PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA,
|
||||||
CalendarEventDevice,
|
CalendarEntity,
|
||||||
|
CalendarEvent,
|
||||||
extract_offset,
|
extract_offset,
|
||||||
get_date,
|
|
||||||
is_offset_reached,
|
is_offset_reached,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -104,7 +104,7 @@ def setup_platform(
|
|||||||
device_id = f"{cust_calendar[CONF_CALENDAR]} {cust_calendar[CONF_NAME]}"
|
device_id = f"{cust_calendar[CONF_CALENDAR]} {cust_calendar[CONF_NAME]}"
|
||||||
entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass)
|
entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass)
|
||||||
calendar_devices.append(
|
calendar_devices.append(
|
||||||
WebDavCalendarEventDevice(
|
WebDavCalendarEntity(
|
||||||
name, calendar, entity_id, days, True, cust_calendar[CONF_SEARCH]
|
name, calendar, entity_id, days, True, cust_calendar[CONF_SEARCH]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -115,24 +115,24 @@ def setup_platform(
|
|||||||
device_id = calendar.name
|
device_id = calendar.name
|
||||||
entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass)
|
entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass)
|
||||||
calendar_devices.append(
|
calendar_devices.append(
|
||||||
WebDavCalendarEventDevice(name, calendar, entity_id, days)
|
WebDavCalendarEntity(name, calendar, entity_id, days)
|
||||||
)
|
)
|
||||||
|
|
||||||
add_entities(calendar_devices, True)
|
add_entities(calendar_devices, True)
|
||||||
|
|
||||||
|
|
||||||
class WebDavCalendarEventDevice(CalendarEventDevice):
|
class WebDavCalendarEntity(CalendarEntity):
|
||||||
"""A device for getting the next Task from a WebDav Calendar."""
|
"""A device for getting the next Task from a WebDav Calendar."""
|
||||||
|
|
||||||
def __init__(self, name, calendar, entity_id, days, all_day=False, search=None):
|
def __init__(self, name, calendar, entity_id, days, all_day=False, search=None):
|
||||||
"""Create the WebDav Calendar Event Device."""
|
"""Create the WebDav Calendar Event Device."""
|
||||||
self.data = WebDavCalendarData(calendar, days, all_day, search)
|
self.data = WebDavCalendarData(calendar, days, all_day, search)
|
||||||
self.entity_id = entity_id
|
self.entity_id = entity_id
|
||||||
self._event = None
|
self._event: CalendarEvent | None = None
|
||||||
self._attr_name = name
|
self._attr_name = name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def event(self):
|
def event(self) -> CalendarEvent | None:
|
||||||
"""Return the next upcoming event."""
|
"""Return the next upcoming event."""
|
||||||
return self._event
|
return self._event
|
||||||
|
|
||||||
@ -147,11 +147,11 @@ class WebDavCalendarEventDevice(CalendarEventDevice):
|
|||||||
if event is None:
|
if event is None:
|
||||||
self._event = event
|
self._event = event
|
||||||
return
|
return
|
||||||
(summary, offset) = extract_offset(event["summary"], OFFSET)
|
(summary, offset) = extract_offset(event.summary, OFFSET)
|
||||||
event["summary"] = summary
|
event.summary = summary
|
||||||
self._event = event
|
self._event = event
|
||||||
self._attr_extra_state_attributes = {
|
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.search = search
|
||||||
self.event = None
|
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 all events in a specific time frame."""
|
||||||
# Get event list from the current calendar
|
# Get event list from the current calendar
|
||||||
vevent_list = await hass.async_add_executor_job(
|
vevent_list = await hass.async_add_executor_job(
|
||||||
@ -180,22 +182,15 @@ class WebDavCalendarData:
|
|||||||
vevent = event.instance.vevent
|
vevent = event.instance.vevent
|
||||||
if not self.is_matching(vevent, self.search):
|
if not self.is_matching(vevent, self.search):
|
||||||
continue
|
continue
|
||||||
uid = None
|
event_list.append(
|
||||||
if hasattr(vevent, "uid"):
|
CalendarEvent(
|
||||||
uid = vevent.uid.value
|
summary=vevent.summary.value,
|
||||||
data = {
|
start=vevent.dtstart.value,
|
||||||
"uid": uid,
|
end=self.get_end_date(vevent),
|
||||||
"summary": vevent.summary.value,
|
location=self.get_attr_value(vevent, "location"),
|
||||||
"start": self.get_hass_date(vevent.dtstart.value),
|
description=self.get_attr_value(vevent, "description"),
|
||||||
"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)
|
|
||||||
|
|
||||||
return event_list
|
return event_list
|
||||||
|
|
||||||
@ -269,13 +264,13 @@ class WebDavCalendarData:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Populate the entity attributes with the event values
|
# Populate the entity attributes with the event values
|
||||||
self.event = {
|
self.event = CalendarEvent(
|
||||||
"summary": vevent.summary.value,
|
summary=vevent.summary.value,
|
||||||
"start": self.get_hass_date(vevent.dtstart.value),
|
start=vevent.dtstart.value,
|
||||||
"end": self.get_hass_date(self.get_end_date(vevent)),
|
end=self.get_end_date(vevent),
|
||||||
"location": self.get_attr_value(vevent, "location"),
|
location=self.get_attr_value(vevent, "location"),
|
||||||
"description": self.get_attr_value(vevent, "description"),
|
description=self.get_attr_value(vevent, "description"),
|
||||||
}
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_matching(vevent, search):
|
def is_matching(vevent, search):
|
||||||
@ -305,14 +300,6 @@ class WebDavCalendarData:
|
|||||||
WebDavCalendarData.get_end_date(vevent)
|
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
|
@staticmethod
|
||||||
def to_datetime(obj):
|
def to_datetime(obj):
|
||||||
"""Return a datetime."""
|
"""Return a datetime."""
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Support for Google Calendar event device sensors."""
|
"""Support for Google Calendar event device sensors."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
import datetime
|
import datetime
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import logging
|
import logging
|
||||||
@ -73,6 +74,48 @@ def get_date(date: dict[str, Any]) -> datetime.datetime:
|
|||||||
return dt.as_local(parsed_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]:
|
def normalize_event(event: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Normalize a calendar event."""
|
"""Normalize a calendar event."""
|
||||||
normalized_event: dict[str, Any] = {}
|
normalized_event: dict[str, Any] = {}
|
||||||
@ -132,7 +175,15 @@ def is_offset_reached(
|
|||||||
|
|
||||||
|
|
||||||
class CalendarEventDevice(Entity):
|
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
|
@property
|
||||||
def event(self) -> dict[str, Any] | None:
|
def event(self) -> dict[str, Any] | None:
|
||||||
@ -143,6 +194,7 @@ class CalendarEventDevice(Entity):
|
|||||||
@property
|
@property
|
||||||
def state_attributes(self) -> dict[str, Any] | None:
|
def state_attributes(self) -> dict[str, Any] | None:
|
||||||
"""Return the entity state attributes."""
|
"""Return the entity state attributes."""
|
||||||
|
|
||||||
if (event := self.event) is None:
|
if (event := self.event) is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -186,6 +238,53 @@ class CalendarEventDevice(Entity):
|
|||||||
raise NotImplementedError()
|
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):
|
class CalendarEventView(http.HomeAssistantView):
|
||||||
"""View to retrieve calendar content."""
|
"""View to retrieve calendar content."""
|
||||||
|
|
||||||
@ -203,7 +302,6 @@ class CalendarEventView(http.HomeAssistantView):
|
|||||||
end = request.query.get("end")
|
end = request.query.get("end")
|
||||||
if start is None or end is None or entity is None:
|
if start is None or end is None or entity is None:
|
||||||
return web.Response(status=HTTPStatus.BAD_REQUEST)
|
return web.Response(status=HTTPStatus.BAD_REQUEST)
|
||||||
assert isinstance(entity, CalendarEventDevice)
|
|
||||||
try:
|
try:
|
||||||
start_date = dt.parse_datetime(start)
|
start_date = dt.parse_datetime(start)
|
||||||
end_date = dt.parse_datetime(end)
|
end_date = dt.parse_datetime(end)
|
||||||
@ -211,10 +309,30 @@ class CalendarEventView(http.HomeAssistantView):
|
|||||||
return web.Response(status=HTTPStatus.BAD_REQUEST)
|
return web.Response(status=HTTPStatus.BAD_REQUEST)
|
||||||
if start_date is None or end_date is None:
|
if start_date is None or end_date is None:
|
||||||
return web.Response(status=HTTPStatus.BAD_REQUEST)
|
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
|
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):
|
class CalendarListView(http.HomeAssistantView):
|
||||||
|
@ -2,8 +2,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
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.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
@ -17,37 +23,66 @@ def setup_platform(
|
|||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Demo Calendar platform."""
|
"""Set up the Demo Calendar platform."""
|
||||||
calendar_data_future = DemoGoogleCalendarDataFuture()
|
|
||||||
calendar_data_current = DemoGoogleCalendarDataCurrent()
|
|
||||||
add_entities(
|
add_entities(
|
||||||
[
|
[
|
||||||
DemoGoogleCalendar(hass, calendar_data_future, "Calendar 1"),
|
DemoCalendar(calendar_data_future(), "Calendar 1"),
|
||||||
DemoGoogleCalendar(hass, calendar_data_current, "Calendar 2"),
|
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."""
|
"""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):
|
@property
|
||||||
"""Get all events in a specific time frame."""
|
def event(self) -> CalendarEvent:
|
||||||
event = copy.copy(self.event)
|
"""Return the next upcoming event."""
|
||||||
event["title"] = event["summary"]
|
return self._event
|
||||||
event["start"] = get_date(event["start"]).isoformat()
|
|
||||||
event["end"] = get_date(event["end"]).isoformat()
|
@property
|
||||||
return [event]
|
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):
|
class LegacyDemoCalendar(CalendarEventDevice):
|
||||||
"""Representation of a Demo Calendar for a future event."""
|
"""Calendar for exercising shim API."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, name):
|
||||||
"""Set the event to a future event."""
|
"""Initialize demo calendar."""
|
||||||
|
self._name = name
|
||||||
one_hour_from_now = dt_util.now() + dt_util.dt.timedelta(minutes=30)
|
one_hour_from_now = dt_util.now() + dt_util.dt.timedelta(minutes=30)
|
||||||
self.event = {
|
self._event = {
|
||||||
"start": {"dateTime": one_hour_from_now.isoformat()},
|
"start": {"dateTime": one_hour_from_now.isoformat()},
|
||||||
"end": {
|
"end": {
|
||||||
"dateTime": (
|
"dateTime": (
|
||||||
@ -57,36 +92,10 @@ class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData):
|
|||||||
"summary": "Future Event",
|
"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
|
@property
|
||||||
def event(self):
|
def event(self):
|
||||||
"""Return the next upcoming event."""
|
"""Return the next upcoming event."""
|
||||||
return self.data.event
|
return self._event
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -94,5 +103,9 @@ class DemoGoogleCalendar(CalendarEventDevice):
|
|||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
async def async_get_events(self, hass, start_date, end_date):
|
async def async_get_events(self, hass, start_date, end_date):
|
||||||
"""Return calendar events within a datetime range."""
|
"""Get all events in a specific time frame."""
|
||||||
return await self.data.async_get_events(hass, start_date, end_date)
|
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]
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
from datetime import datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -10,9 +10,9 @@ from httplib2 import ServerNotFoundError
|
|||||||
|
|
||||||
from homeassistant.components.calendar import (
|
from homeassistant.components.calendar import (
|
||||||
ENTITY_ID_FORMAT,
|
ENTITY_ID_FORMAT,
|
||||||
CalendarEventDevice,
|
CalendarEntity,
|
||||||
|
CalendarEvent,
|
||||||
extract_offset,
|
extract_offset,
|
||||||
get_date,
|
|
||||||
is_offset_reached,
|
is_offset_reached,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
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.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import generate_entity_id
|
from homeassistant.helpers.entity import generate_entity_id
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle, dt
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
CONF_CAL_ID,
|
CONF_CAL_ID,
|
||||||
@ -94,7 +94,7 @@ def _async_setup_entities(
|
|||||||
entity_id = generate_entity_id(
|
entity_id = generate_entity_id(
|
||||||
ENTITY_ID_FORMAT, data[CONF_DEVICE_ID], hass=hass
|
ENTITY_ID_FORMAT, data[CONF_DEVICE_ID], hass=hass
|
||||||
)
|
)
|
||||||
entity = GoogleCalendarEventDevice(
|
entity = GoogleCalendarEntity(
|
||||||
calendar_service, disc_info[CONF_CAL_ID], data, entity_id
|
calendar_service, disc_info[CONF_CAL_ID], data, entity_id
|
||||||
)
|
)
|
||||||
entities.append(entity)
|
entities.append(entity)
|
||||||
@ -102,7 +102,7 @@ def _async_setup_entities(
|
|||||||
async_add_entities(entities, True)
|
async_add_entities(entities, True)
|
||||||
|
|
||||||
|
|
||||||
class GoogleCalendarEventDevice(CalendarEventDevice):
|
class GoogleCalendarEntity(CalendarEntity):
|
||||||
"""A calendar event device."""
|
"""A calendar event device."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -117,7 +117,7 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
|
|||||||
self._calendar_id = calendar_id
|
self._calendar_id = calendar_id
|
||||||
self._search: str | None = data.get(CONF_SEARCH)
|
self._search: str | None = data.get(CONF_SEARCH)
|
||||||
self._ignore_availability: bool = data.get(CONF_IGNORE_AVAILABILITY, False)
|
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._name: str = data[CONF_NAME]
|
||||||
self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET)
|
self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET)
|
||||||
self._offset_reached = False
|
self._offset_reached = False
|
||||||
@ -129,7 +129,7 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
|
|||||||
return {"offset_reached": self._offset_reached}
|
return {"offset_reached": self._offset_reached}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def event(self) -> dict[str, Any] | None:
|
def event(self) -> CalendarEvent | None:
|
||||||
"""Return the next upcoming event."""
|
"""Return the next upcoming event."""
|
||||||
return self._event
|
return self._event
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
|
|||||||
|
|
||||||
async def async_get_events(
|
async def async_get_events(
|
||||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[CalendarEvent]:
|
||||||
"""Get all events in a specific time frame."""
|
"""Get all events in a specific time frame."""
|
||||||
event_list: list[dict[str, Any]] = []
|
event_list: list[dict[str, Any]] = []
|
||||||
page_token: str | None = None
|
page_token: str | None = None
|
||||||
@ -166,7 +166,8 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
|
|||||||
event_list.extend(filter(self._event_filter, items))
|
event_list.extend(filter(self._event_filter, items))
|
||||||
if not page_token:
|
if not page_token:
|
||||||
break
|
break
|
||||||
return event_list
|
|
||||||
|
return [_get_calendar_event(event) for event in event_list]
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
@ -181,12 +182,35 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
|
|||||||
|
|
||||||
# Pick the first visible event and apply offset calculations.
|
# Pick the first visible event and apply offset calculations.
|
||||||
valid_items = filter(self._event_filter, items)
|
valid_items = filter(self._event_filter, items)
|
||||||
self._event = copy.deepcopy(next(valid_items, None))
|
event = copy.deepcopy(next(valid_items, None))
|
||||||
if self._event:
|
if event:
|
||||||
(summary, offset) = extract_offset(
|
(summary, offset) = extract_offset(event.get("summary", ""), self._offset)
|
||||||
self._event.get("summary", ""), self._offset
|
event["summary"] = summary
|
||||||
)
|
self._event = _get_calendar_event(event)
|
||||||
self._event["summary"] = summary
|
|
||||||
self._offset_reached = is_offset_reached(
|
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"),
|
||||||
|
)
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
"""Support for Todoist task management (https://todoist.com)."""
|
"""Support for Todoist task management (https://todoist.com)."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import date, datetime, timedelta, timezone
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from todoist.api import TodoistAPI
|
from todoist.api import TodoistAPI
|
||||||
import voluptuous as vol
|
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.const import CONF_ID, CONF_NAME, CONF_TOKEN
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.template import DATE_STR_FORMAT
|
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
from homeassistant.util import dt
|
from homeassistant.util import dt
|
||||||
|
|
||||||
@ -28,7 +31,6 @@ from .const import (
|
|||||||
CONF_PROJECT_LABEL_WHITELIST,
|
CONF_PROJECT_LABEL_WHITELIST,
|
||||||
CONF_PROJECT_WHITELIST,
|
CONF_PROJECT_WHITELIST,
|
||||||
CONTENT,
|
CONTENT,
|
||||||
DATETIME,
|
|
||||||
DESCRIPTION,
|
DESCRIPTION,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
DUE,
|
DUE,
|
||||||
@ -102,7 +104,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(minutes=15)
|
SCAN_INTERVAL = timedelta(minutes=1)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(
|
def setup_platform(
|
||||||
@ -136,7 +138,7 @@ def setup_platform(
|
|||||||
# Project is an object, not a dict!
|
# Project is an object, not a dict!
|
||||||
# Because of that, we convert what we need to a dict.
|
# Because of that, we convert what we need to a dict.
|
||||||
project_data = {CONF_NAME: project[NAME], CONF_ID: project[ID]}
|
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.
|
# Cache the names so we can easily look up name->ID.
|
||||||
project_id_lookup[project[NAME].lower()] = project[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.
|
# Create the custom project and add it to the devices array.
|
||||||
project_devices.append(
|
project_devices.append(
|
||||||
TodoistProjectDevice(
|
TodoistProjectEntity(
|
||||||
hass,
|
hass,
|
||||||
project,
|
project,
|
||||||
labels,
|
labels,
|
||||||
@ -176,7 +178,6 @@ def setup_platform(
|
|||||||
project_id_filter,
|
project_id_filter,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
add_entities(project_devices)
|
add_entities(project_devices)
|
||||||
|
|
||||||
def handle_new_task(call: ServiceCall) -> None:
|
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)
|
return dt.as_utc(nowtime)
|
||||||
|
|
||||||
|
|
||||||
class TodoistProjectDevice(CalendarEventDevice):
|
class TodoistProjectEntity(CalendarEntity):
|
||||||
"""A device for getting the next Task from a Todoist Project."""
|
"""A device for getting the next Task from a Todoist Project."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -284,7 +285,7 @@ class TodoistProjectDevice(CalendarEventDevice):
|
|||||||
whitelisted_labels=None,
|
whitelisted_labels=None,
|
||||||
whitelisted_projects=None,
|
whitelisted_projects=None,
|
||||||
):
|
):
|
||||||
"""Create the Todoist Calendar Event Device."""
|
"""Create the Todoist Calendar Entity."""
|
||||||
self.data = TodoistProjectData(
|
self.data = TodoistProjectData(
|
||||||
data,
|
data,
|
||||||
labels,
|
labels,
|
||||||
@ -297,9 +298,9 @@ class TodoistProjectDevice(CalendarEventDevice):
|
|||||||
self._name = data[CONF_NAME]
|
self._name = data[CONF_NAME]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def event(self):
|
def event(self) -> CalendarEvent:
|
||||||
"""Return the next upcoming event."""
|
"""Return the next upcoming event."""
|
||||||
return self.data.event
|
return self.data.calendar_event
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -314,7 +315,12 @@ class TodoistProjectDevice(CalendarEventDevice):
|
|||||||
task[SUMMARY] for task in self.data.all_project_tasks
|
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."""
|
"""Get all events in a specific time frame."""
|
||||||
return await self.data.async_get_events(hass, start_date, end_date)
|
return await self.data.async_get_events(hass, start_date, end_date)
|
||||||
|
|
||||||
@ -336,7 +342,7 @@ class TodoistProjectDevice(CalendarEventDevice):
|
|||||||
|
|
||||||
class TodoistProjectData:
|
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
|
This is analogous to the GoogleCalendarData found in the Google Calendar
|
||||||
component.
|
component.
|
||||||
@ -409,6 +415,22 @@ class TodoistProjectData:
|
|||||||
else:
|
else:
|
||||||
self._project_id_whitelist = []
|
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):
|
def create_todoist_task(self, data):
|
||||||
"""
|
"""
|
||||||
Create a dictionary based on a Task passed from the Todoist API.
|
Create a dictionary based on a Task passed from the Todoist API.
|
||||||
@ -566,7 +588,7 @@ class TodoistProjectData:
|
|||||||
if task["due"] is None:
|
if task["due"] is None:
|
||||||
continue
|
continue
|
||||||
# @NOTE: _parse_due_date always returns the date in UTC time.
|
# @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"]
|
task["due"], self._api.state["user"]["tz_info"]["hours"]
|
||||||
)
|
)
|
||||||
if not due_date:
|
if not due_date:
|
||||||
@ -580,20 +602,16 @@ class TodoistProjectData:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if start_date < due_date < end_date:
|
if start_date < due_date < end_date:
|
||||||
|
due_date_value: datetime | date = due_date
|
||||||
if due_date == midnight:
|
if due_date == midnight:
|
||||||
# If the due date has no time data, return just the date so that it
|
# 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.
|
# will render correctly as an all day event on a calendar.
|
||||||
due_date_value = due_date.strftime("%Y-%m-%d")
|
due_date_value = due_date.date()
|
||||||
else:
|
event = CalendarEvent(
|
||||||
due_date_value = due_date.isoformat()
|
summary=task["content"],
|
||||||
event = {
|
start=due_date_value,
|
||||||
"uid": task["id"],
|
end=due_date_value,
|
||||||
"title": task["content"],
|
)
|
||||||
"start": due_date_value,
|
|
||||||
"end": due_date_value,
|
|
||||||
"allDay": True,
|
|
||||||
"summary": task["content"],
|
|
||||||
}
|
|
||||||
events.append(event)
|
events.append(event)
|
||||||
return events
|
return events
|
||||||
|
|
||||||
@ -644,22 +662,10 @@ class TodoistProjectData:
|
|||||||
project_tasks.remove(best_task)
|
project_tasks.remove(best_task)
|
||||||
self.all_project_tasks.append(best_task)
|
self.all_project_tasks.append(best_task)
|
||||||
|
|
||||||
self.event = self.all_project_tasks[0]
|
event = self.all_project_tasks[0]
|
||||||
|
if event is None or event[START] is None:
|
||||||
# Convert datetime to a string again
|
_LOGGER.debug("No valid event or event start for %s", self._name)
|
||||||
if self.event is not None:
|
self.event = None
|
||||||
if self.event[START] is not None:
|
return
|
||||||
self.event[START] = {
|
self.event = event
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_LOGGER.debug("Updated %s", self._name)
|
_LOGGER.debug("Updated %s", self._name)
|
||||||
|
@ -2,9 +2,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
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.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_ID
|
from homeassistant.const import CONF_ID
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
@ -26,7 +25,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities([TwenteMilieuCalendar(coordinator, entry)])
|
async_add_entities([TwenteMilieuCalendar(coordinator, entry)])
|
||||||
|
|
||||||
|
|
||||||
class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEventDevice):
|
class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEntity):
|
||||||
"""Defines a Twente Milieu calendar."""
|
"""Defines a Twente Milieu calendar."""
|
||||||
|
|
||||||
_attr_name = "Twente Milieu"
|
_attr_name = "Twente Milieu"
|
||||||
@ -40,26 +39,25 @@ class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEventDevice):
|
|||||||
"""Initialize the Twente Milieu entity."""
|
"""Initialize the Twente Milieu entity."""
|
||||||
super().__init__(coordinator, entry)
|
super().__init__(coordinator, entry)
|
||||||
self._attr_unique_id = str(entry.data[CONF_ID])
|
self._attr_unique_id = str(entry.data[CONF_ID])
|
||||||
self._event: dict[str, Any] | None = None
|
self._event: CalendarEvent | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def event(self) -> dict[str, Any] | None:
|
def event(self) -> CalendarEvent | None:
|
||||||
"""Return the next upcoming event."""
|
"""Return the next upcoming event."""
|
||||||
return self._event
|
return self._event
|
||||||
|
|
||||||
async def async_get_events(
|
async def async_get_events(
|
||||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[CalendarEvent]:
|
||||||
"""Return calendar events within a datetime range."""
|
"""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():
|
for waste_type, waste_dates in self.coordinator.data.items():
|
||||||
events.extend(
|
events.extend(
|
||||||
{
|
CalendarEvent(
|
||||||
"all_day": True,
|
summary=WASTE_TYPE_TO_DESCRIPTION[waste_type],
|
||||||
"start": {"date": waste_date.isoformat()},
|
start=waste_date,
|
||||||
"end": {"date": waste_date.isoformat()},
|
end=waste_date,
|
||||||
"summary": WASTE_TYPE_TO_DESCRIPTION[waste_type],
|
)
|
||||||
}
|
|
||||||
for waste_date in waste_dates
|
for waste_date in waste_dates
|
||||||
if start_date.date() <= waste_date <= end_date.date()
|
if start_date.date() <= waste_date <= end_date.date()
|
||||||
)
|
)
|
||||||
@ -86,12 +84,11 @@ class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEventDevice):
|
|||||||
|
|
||||||
self._event = None
|
self._event = None
|
||||||
if next_waste_pickup_date is not None and next_waste_pickup_type is not None:
|
if next_waste_pickup_date is not None and next_waste_pickup_type is not None:
|
||||||
self._event = {
|
self._event = CalendarEvent(
|
||||||
"all_day": True,
|
summary=WASTE_TYPE_TO_DESCRIPTION[next_waste_pickup_type],
|
||||||
"start": {"date": next_waste_pickup_date.isoformat()},
|
start=next_waste_pickup_date,
|
||||||
"end": {"date": next_waste_pickup_date.isoformat()},
|
end=next_waste_pickup_date,
|
||||||
"summary": WASTE_TYPE_TO_DESCRIPTION[next_waste_pickup_type],
|
)
|
||||||
}
|
|
||||||
|
|
||||||
super()._handle_coordinator_update()
|
super()._handle_coordinator_update()
|
||||||
|
|
||||||
|
@ -934,11 +934,8 @@ async def test_get_events_custom_calendars(hass, calendar, get_api_events):
|
|||||||
events = await get_api_events("calendar.private_private")
|
events = await get_api_events("calendar.private_private")
|
||||||
assert events == [
|
assert events == [
|
||||||
{
|
{
|
||||||
"description": "Surprisingly rainy",
|
"end": {"dateTime": "2017-11-27T10:00:00-08:00"},
|
||||||
"end": "2017-11-27T10:00:00-08:00",
|
"start": {"dateTime": "2017-11-27T09:00:00-08:00"},
|
||||||
"location": "Hamburg",
|
|
||||||
"start": "2017-11-27T09:00:00-08:00",
|
|
||||||
"summary": "This is a normal event",
|
"summary": "This is a normal event",
|
||||||
"uid": "1",
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -23,7 +23,6 @@ async def test_events_http_api(hass, hass_client):
|
|||||||
assert response.status == HTTPStatus.OK
|
assert response.status == HTTPStatus.OK
|
||||||
events = await response.json()
|
events = await response.json()
|
||||||
assert events[0]["summary"] == "Future Event"
|
assert events[0]["summary"] == "Future Event"
|
||||||
assert events[0]["title"] == "Future Event"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_calendars_http_api(hass, hass_client):
|
async def test_calendars_http_api(hass, hass_client):
|
||||||
@ -37,4 +36,24 @@ async def test_calendars_http_api(hass, hass_client):
|
|||||||
assert data == [
|
assert data == [
|
||||||
{"entity_id": "calendar.calendar_1", "name": "Calendar 1"},
|
{"entity_id": "calendar.calendar_1", "name": "Calendar 1"},
|
||||||
{"entity_id": "calendar.calendar_2", "name": "Calendar 2"},
|
{"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"
|
||||||
|
@ -76,7 +76,6 @@ async def test_api_events(
|
|||||||
events = await response.json()
|
events = await response.json()
|
||||||
assert len(events) == 1
|
assert len(events) == 1
|
||||||
assert events[0] == {
|
assert events[0] == {
|
||||||
"all_day": True,
|
|
||||||
"start": {"date": "2022-01-06"},
|
"start": {"date": "2022-01-06"},
|
||||||
"end": {"date": "2022-01-06"},
|
"end": {"date": "2022-01-06"},
|
||||||
"summary": "Christmas Tree Pickup",
|
"summary": "Christmas Tree Pickup",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user