Added ics_calender custom component.
This commit is contained in:
1
custom_components/ics_calendar/parsers/__init__.py
Normal file
1
custom_components/ics_calendar/parsers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Provide parsers."""
|
Binary file not shown.
Binary file not shown.
190
custom_components/ics_calendar/parsers/parser_ics.py
Normal file
190
custom_components/ics_calendar/parsers/parser_ics.py
Normal file
@@ -0,0 +1,190 @@
|
||||
"""Support for ics parser."""
|
||||
import re
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Optional, Union
|
||||
|
||||
from arrow import Arrow, get as arrowget
|
||||
from homeassistant.components.calendar import CalendarEvent
|
||||
from ics import Calendar
|
||||
|
||||
from ..filter import Filter
|
||||
from ..icalendarparser import ICalendarParser
|
||||
from ..utility import compare_event_dates
|
||||
|
||||
|
||||
class ParserICS(ICalendarParser):
|
||||
"""Class to provide parser using ics module."""
|
||||
|
||||
def __init__(self):
|
||||
"""Construct ParserICS."""
|
||||
self._re_method = re.compile("^METHOD:.*$", flags=re.MULTILINE)
|
||||
self._calendar = None
|
||||
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(re.sub(self._re_method, "", 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, end, 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:
|
||||
# ics 0.8 takes datetime not Arrow objects
|
||||
# ar_start = start
|
||||
# ar_end = end
|
||||
ar_start = arrowget(start - timedelta(hours=offset_hours))
|
||||
ar_end = arrowget(end - timedelta(hours=offset_hours))
|
||||
|
||||
for event in self._calendar.timeline.included(ar_start, ar_end):
|
||||
if event.all_day and not include_all_day:
|
||||
continue
|
||||
summary: str = ""
|
||||
# ics 0.8 uses 'summary' reliably, older versions use 'name'
|
||||
# if hasattr(event, "summary"):
|
||||
# summary = event.summary
|
||||
# elif hasattr(event, "name"):
|
||||
summary = event.name
|
||||
calendar_event: CalendarEvent = CalendarEvent(
|
||||
summary=summary,
|
||||
start=ParserICS.get_date(
|
||||
event.begin, event.all_day, offset_hours
|
||||
),
|
||||
end=ParserICS.get_date(
|
||||
event.end, event.all_day, offset_hours
|
||||
),
|
||||
location=event.location,
|
||||
description=event.description,
|
||||
)
|
||||
if self._filter.filter_event(calendar_event):
|
||||
event_list.append(calendar_event)
|
||||
|
||||
return event_list
|
||||
|
||||
def get_current_event( # noqa: $701
|
||||
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 int
|
||||
:returns a CalendarEvent or None
|
||||
"""
|
||||
if self._calendar is None:
|
||||
return None
|
||||
|
||||
temp_event = None
|
||||
now = now - timedelta(offset_hours)
|
||||
end = now + timedelta(days=days)
|
||||
for event in self._calendar.timeline.included(
|
||||
arrowget(now), arrowget(end)
|
||||
):
|
||||
if event.all_day and not include_all_day:
|
||||
continue
|
||||
|
||||
if not self._filter.filter(event.name, event.description):
|
||||
continue
|
||||
|
||||
if temp_event is None or compare_event_dates(
|
||||
now,
|
||||
temp_event.end,
|
||||
temp_event.begin,
|
||||
temp_event.all_day,
|
||||
event.end,
|
||||
event.begin,
|
||||
event.all_day,
|
||||
):
|
||||
temp_event = event
|
||||
|
||||
if temp_event is None:
|
||||
return None
|
||||
# if hasattr(event, "summary"):
|
||||
# summary = temp_event.summary
|
||||
# elif hasattr(event, "name"):
|
||||
summary = temp_event.name
|
||||
return CalendarEvent(
|
||||
summary=summary,
|
||||
start=ParserICS.get_date(
|
||||
temp_event.begin, temp_event.all_day, offset_hours
|
||||
),
|
||||
end=ParserICS.get_date(
|
||||
temp_event.end, temp_event.all_day, offset_hours
|
||||
),
|
||||
location=temp_event.location,
|
||||
description=temp_event.description,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_date(
|
||||
arw: Arrow, is_all_day: bool, offset_hours: int
|
||||
) -> Union[datetime, date]:
|
||||
"""Get datetime.
|
||||
|
||||
:param arw The arrow object representing the date.
|
||||
:type Arrow
|
||||
:param is_all_day If true, the returned datetime will have the time
|
||||
component set to 0.
|
||||
:type: bool
|
||||
:param offset_hours the number of hours to offset the event
|
||||
:type int
|
||||
:returns The datetime.
|
||||
:rtype datetime
|
||||
"""
|
||||
# if isinstance(arw, Arrow):
|
||||
if is_all_day:
|
||||
return arw.date()
|
||||
# else:
|
||||
# if arw.tzinfo is None or arw.tzinfo.utcoffset(arw) is None
|
||||
# or is_all_day:
|
||||
# arw = arw.astimezone()
|
||||
# if is_all_day:
|
||||
# return arw.date()
|
||||
#
|
||||
arw = arw.shift(hours=offset_hours)
|
||||
|
||||
return_value = arw.datetime
|
||||
if return_value.tzinfo is None:
|
||||
return_value = return_value.astimezone()
|
||||
return return_value
|
198
custom_components/ics_calendar/parsers/parser_rie.py
Normal file
198
custom_components/ics_calendar/parsers/parser_rie.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""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
|
Reference in New Issue
Block a user