199 lines
6.7 KiB
Python
199 lines
6.7 KiB
Python
|
"""Support for recurring_ical_events parser."""
|
||
|
from datetime import date, datetime, timedelta
|
||
|
from typing import Optional, Union
|
||
|
|
||
|
import recurring_ical_events as rie
|
||
|
from homeassistant.components.calendar import CalendarEvent
|
||
|
from icalendar import Calendar
|
||
|
|
||
|
from ..filter import Filter
|
||
|
from ..icalendarparser import ICalendarParser
|
||
|
from ..utility import compare_event_dates
|
||
|
|
||
|
|
||
|
class ParserRIE(ICalendarParser):
|
||
|
"""Provide parser using recurring_ical_events."""
|
||
|
|
||
|
def __init__(self):
|
||
|
"""Construct ParserRIE."""
|
||
|
self._calendar = None
|
||
|
self.oneday = timedelta(days=1)
|
||
|
self.oneday2 = timedelta(hours=23, minutes=59, seconds=59)
|
||
|
self._filter = Filter("", "")
|
||
|
|
||
|
def set_content(self, content: str):
|
||
|
"""Parse content into a calendar object.
|
||
|
|
||
|
This must be called at least once before get_event_list or
|
||
|
get_current_event.
|
||
|
:param content is the calendar data
|
||
|
:type content str
|
||
|
"""
|
||
|
self._calendar = Calendar.from_ical(content)
|
||
|
|
||
|
def set_filter(self, filt: Filter):
|
||
|
"""Set a Filter object to filter events.
|
||
|
|
||
|
:param filt: The Filter object
|
||
|
:type exclude: Filter
|
||
|
"""
|
||
|
self._filter = filt
|
||
|
|
||
|
def get_event_list(
|
||
|
self,
|
||
|
start: datetime,
|
||
|
end: datetime,
|
||
|
include_all_day: bool,
|
||
|
offset_hours: int = 0,
|
||
|
) -> list[CalendarEvent]:
|
||
|
"""Get a list of events.
|
||
|
|
||
|
Gets the events from start to end, including or excluding all day
|
||
|
events.
|
||
|
:param start the earliest start time of events to return
|
||
|
:type datetime
|
||
|
:param end the latest start time of events to return
|
||
|
:type datetime
|
||
|
:param include_all_day if true, all day events will be included.
|
||
|
:type boolean
|
||
|
:param offset_hours the number of hours to offset the event
|
||
|
:type offset_hours int
|
||
|
:returns a list of events, or an empty list
|
||
|
:rtype list[CalendarEvent]
|
||
|
"""
|
||
|
event_list: list[CalendarEvent] = []
|
||
|
|
||
|
if self._calendar is not None:
|
||
|
for event in rie.of(self._calendar).between(
|
||
|
start - timedelta(hours=offset_hours),
|
||
|
end - timedelta(hours=offset_hours),
|
||
|
):
|
||
|
start, end, all_day = self.is_all_day(event, offset_hours)
|
||
|
|
||
|
if all_day and not include_all_day:
|
||
|
continue
|
||
|
|
||
|
calendar_event: CalendarEvent = CalendarEvent(
|
||
|
summary=event.get("SUMMARY"),
|
||
|
start=start,
|
||
|
end=end,
|
||
|
location=event.get("LOCATION"),
|
||
|
description=event.get("DESCRIPTION"),
|
||
|
)
|
||
|
if self._filter.filter_event(calendar_event):
|
||
|
event_list.append(calendar_event)
|
||
|
|
||
|
return event_list
|
||
|
|
||
|
def get_current_event( # noqa: R701
|
||
|
self,
|
||
|
include_all_day: bool,
|
||
|
now: datetime,
|
||
|
days: int,
|
||
|
offset_hours: int = 0,
|
||
|
) -> Optional[CalendarEvent]:
|
||
|
"""Get the current or next event.
|
||
|
|
||
|
Gets the current event, or the next upcoming event with in the
|
||
|
specified number of days, if there is no current event.
|
||
|
:param include_all_day if true, all day events will be included.
|
||
|
:type boolean
|
||
|
:param now the current date and time
|
||
|
:type datetime
|
||
|
:param days the number of days to check for an upcoming event
|
||
|
:type int
|
||
|
:param offset_hours the number of hours to offset the event
|
||
|
:type offset_hours int
|
||
|
:returns a CalendarEvent or None
|
||
|
"""
|
||
|
if self._calendar is None:
|
||
|
return None
|
||
|
|
||
|
temp_event: CalendarEvent = None
|
||
|
temp_start: date | datetime = None
|
||
|
temp_end: date | datetime = None
|
||
|
temp_all_day: bool = None
|
||
|
end: datetime = now + timedelta(days=days)
|
||
|
for event in rie.of(self._calendar).between(
|
||
|
now - timedelta(hours=offset_hours),
|
||
|
end - timedelta(hours=offset_hours),
|
||
|
):
|
||
|
start, end, all_day = self.is_all_day(event, offset_hours)
|
||
|
|
||
|
if all_day and not include_all_day:
|
||
|
continue
|
||
|
|
||
|
if not self._filter.filter(
|
||
|
event.get("SUMMARY"), event.get("DESCRIPTION")
|
||
|
):
|
||
|
continue
|
||
|
|
||
|
if temp_start is None or compare_event_dates(
|
||
|
now, temp_end, temp_start, temp_all_day, end, start, all_day
|
||
|
):
|
||
|
temp_event = event
|
||
|
temp_start = start
|
||
|
temp_end = end
|
||
|
temp_all_day = all_day
|
||
|
|
||
|
if temp_event is None:
|
||
|
return None
|
||
|
|
||
|
return CalendarEvent(
|
||
|
summary=temp_event.get("SUMMARY"),
|
||
|
start=temp_start,
|
||
|
end=temp_end,
|
||
|
location=temp_event.get("LOCATION"),
|
||
|
description=temp_event.get("DESCRIPTION"),
|
||
|
)
|
||
|
|
||
|
@staticmethod
|
||
|
def get_date(date_time) -> Union[datetime, date]:
|
||
|
"""Get datetime with timezone information.
|
||
|
|
||
|
If a date object is passed, it will first have a time component added,
|
||
|
set to 0.
|
||
|
:param date_time The date or datetime object
|
||
|
:type date_time datetime or date
|
||
|
:type: bool
|
||
|
:returns The datetime.
|
||
|
:rtype datetime
|
||
|
"""
|
||
|
# Must use type here, since a datetime is also a date!
|
||
|
if isinstance(date_time, date) and not isinstance(date_time, datetime):
|
||
|
date_time = datetime.combine(date_time, datetime.min.time())
|
||
|
return date_time.astimezone()
|
||
|
|
||
|
def is_all_day(self, event, offset_hours: int):
|
||
|
"""Determine if the event is an all day event.
|
||
|
|
||
|
Return all day status and start and end times for the event.
|
||
|
:param event The event to examine
|
||
|
:param offset_hours the number of hours to offset the event
|
||
|
:type offset_hours int
|
||
|
"""
|
||
|
start: datetime | date = ParserRIE.get_date(event.get("DTSTART").dt)
|
||
|
end: datetime | date = ParserRIE.get_date(event.get("DTEND").dt)
|
||
|
all_day = False
|
||
|
diff = event.get("DURATION")
|
||
|
if diff is not None:
|
||
|
diff = diff.dt
|
||
|
else:
|
||
|
diff = end - start
|
||
|
if (start == end or diff in {self.oneday, self.oneday2}) and all(
|
||
|
x == 0 for x in [start.hour, start.minute, start.second]
|
||
|
):
|
||
|
# if all_day, start and end must be date, not datetime!
|
||
|
start = start.date()
|
||
|
end = end.date()
|
||
|
all_day = True
|
||
|
else:
|
||
|
start = start + timedelta(hours=offset_hours)
|
||
|
end = end + timedelta(hours=offset_hours)
|
||
|
if start.tzinfo is None:
|
||
|
start = start.astimezone()
|
||
|
if end.tzinfo is None:
|
||
|
end = end.astimezone()
|
||
|
|
||
|
return start, end, all_day
|