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