Added ics_calender custom component.
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,7 @@ | ||||
| automations_webhooks.yaml | ||||
| spotify.yaml | ||||
| calendars.yaml | ||||
| ics_calendars.yaml | ||||
| esphome/common/secrets.yaml | ||||
| esphome/secrets.yaml | ||||
| secrets.yaml | ||||
|   | ||||
| @@ -42,6 +42,7 @@ template: !include template.yaml | ||||
|  | ||||
| # calendar integration | ||||
| calendar: !include calendars.yaml | ||||
| ics_calendar: !include ics_calendars.yaml | ||||
|  | ||||
| # DB-recorder configuration | ||||
| recorder: !include recorder.yaml | ||||
|   | ||||
							
								
								
									
										113
									
								
								custom_components/ics_calendar/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								custom_components/ics_calendar/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| """ics Calendar for Home Assistant.""" | ||||
|  | ||||
| import logging | ||||
|  | ||||
| import homeassistant.helpers.config_validation as cv | ||||
| import voluptuous as vol | ||||
| from homeassistant.const import ( | ||||
|     CONF_EXCLUDE, | ||||
|     CONF_INCLUDE, | ||||
|     CONF_NAME, | ||||
|     CONF_PASSWORD, | ||||
|     CONF_PREFIX, | ||||
|     CONF_URL, | ||||
|     CONF_USERNAME, | ||||
|     Platform, | ||||
| ) | ||||
| from homeassistant.core import HomeAssistant | ||||
| from homeassistant.helpers.typing import ConfigType | ||||
|  | ||||
| from .const import DOMAIN, UPGRADE_URL | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
| PLATFORMS: list[Platform] = [Platform.CALENDAR] | ||||
|  | ||||
| CONF_DEVICE_ID = "device_id" | ||||
| CONF_CALENDARS = "calendars" | ||||
| CONF_DAYS = "days" | ||||
| CONF_INCLUDE_ALL_DAY = "include_all_day" | ||||
| CONF_PARSER = "parser" | ||||
| CONF_DOWNLOAD_INTERVAL = "download_interval" | ||||
| CONF_USER_AGENT = "user_agent" | ||||
| CONF_OFFSET_HOURS = "offset_hours" | ||||
| CONF_ACCEPT_HEADER = "accept_header" | ||||
|  | ||||
| CONFIG_SCHEMA = vol.Schema( | ||||
|     { | ||||
|         DOMAIN: vol.Schema( | ||||
|             { | ||||
|                 # pylint: disable=no-value-for-parameter | ||||
|                 vol.Optional(CONF_CALENDARS, default=[]): vol.All( | ||||
|                     cv.ensure_list, | ||||
|                     vol.Schema( | ||||
|                         [ | ||||
|                             vol.Schema( | ||||
|                                 { | ||||
|                                     vol.Required(CONF_URL): vol.Url(), | ||||
|                                     vol.Required(CONF_NAME): cv.string, | ||||
|                                     vol.Optional( | ||||
|                                         CONF_INCLUDE_ALL_DAY, default=False | ||||
|                                     ): cv.boolean, | ||||
|                                     vol.Optional( | ||||
|                                         CONF_USERNAME, default="" | ||||
|                                     ): cv.string, | ||||
|                                     vol.Optional( | ||||
|                                         CONF_PASSWORD, default="" | ||||
|                                     ): cv.string, | ||||
|                                     vol.Optional( | ||||
|                                         CONF_PARSER, default="rie" | ||||
|                                     ): cv.string, | ||||
|                                     vol.Optional( | ||||
|                                         CONF_PREFIX, default="" | ||||
|                                     ): cv.string, | ||||
|                                     vol.Optional( | ||||
|                                         CONF_DAYS, default=1 | ||||
|                                     ): cv.positive_int, | ||||
|                                     vol.Optional( | ||||
|                                         CONF_DOWNLOAD_INTERVAL, default=15 | ||||
|                                     ): cv.positive_int, | ||||
|                                     vol.Optional( | ||||
|                                         CONF_USER_AGENT, default="" | ||||
|                                     ): cv.string, | ||||
|                                     vol.Optional( | ||||
|                                         CONF_EXCLUDE, default="" | ||||
|                                     ): cv.string, | ||||
|                                     vol.Optional( | ||||
|                                         CONF_INCLUDE, default="" | ||||
|                                     ): cv.string, | ||||
|                                     vol.Optional( | ||||
|                                         CONF_OFFSET_HOURS, default=0 | ||||
|                                     ): int, | ||||
|                                     vol.Optional( | ||||
|                                         CONF_ACCEPT_HEADER, default="" | ||||
|                                     ): cv.string, | ||||
|                                 } | ||||
|                             ) | ||||
|                         ] | ||||
|                     ), | ||||
|                 ) | ||||
|             } | ||||
|         ) | ||||
|     }, | ||||
|     extra=vol.ALLOW_EXTRA, | ||||
| ) | ||||
|  | ||||
|  | ||||
| def setup(hass: HomeAssistant, config: ConfigType) -> bool: | ||||
|     """Set up calendars.""" | ||||
|     _LOGGER.debug("Setting up ics_calendar component") | ||||
|     hass.data.setdefault(DOMAIN, {}) | ||||
|  | ||||
|     if DOMAIN in config and config[DOMAIN]: | ||||
|         hass.helpers.discovery.load_platform( | ||||
|             PLATFORMS[0], DOMAIN, config[DOMAIN], config | ||||
|         ) | ||||
|     else: | ||||
|         _LOGGER.error( | ||||
|             "No configuration found! If you upgraded from ics_calendar v3.2.0 " | ||||
|             "or older, you need to update your configuration! See " | ||||
|             "%s for more information.", | ||||
|             UPGRADE_URL, | ||||
|         ) | ||||
|  | ||||
|     return True | ||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								custom_components/ics_calendar/__pycache__/const.cpython-311.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								custom_components/ics_calendar/__pycache__/const.cpython-311.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										291
									
								
								custom_components/ics_calendar/calendar.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										291
									
								
								custom_components/ics_calendar/calendar.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,291 @@ | ||||
| """Support for ICS Calendar.""" | ||||
| import logging | ||||
| from datetime import datetime, timedelta | ||||
| from typing import Optional | ||||
|  | ||||
| # import homeassistant.helpers.config_validation as cv | ||||
| # import voluptuous as vol | ||||
| from homeassistant.components.calendar import ( | ||||
|     ENTITY_ID_FORMAT, | ||||
|     CalendarEntity, | ||||
|     CalendarEvent, | ||||
|     extract_offset, | ||||
|     is_offset_reached, | ||||
| ) | ||||
| from homeassistant.const import ( | ||||
|     CONF_EXCLUDE, | ||||
|     CONF_INCLUDE, | ||||
|     CONF_NAME, | ||||
|     CONF_PASSWORD, | ||||
|     CONF_PREFIX, | ||||
|     CONF_URL, | ||||
|     CONF_USERNAME, | ||||
| ) | ||||
| from homeassistant.core import HomeAssistant | ||||
| from homeassistant.helpers.entity import generate_entity_id | ||||
| from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||||
| from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType | ||||
| from homeassistant.util import Throttle | ||||
| from homeassistant.util.dt import now as hanow | ||||
|  | ||||
| from . import ( | ||||
|     CONF_ACCEPT_HEADER, | ||||
|     CONF_CALENDARS, | ||||
|     CONF_DAYS, | ||||
|     CONF_DOWNLOAD_INTERVAL, | ||||
|     CONF_INCLUDE_ALL_DAY, | ||||
|     CONF_OFFSET_HOURS, | ||||
|     CONF_PARSER, | ||||
|     CONF_USER_AGENT, | ||||
| ) | ||||
| from .calendardata import CalendarData | ||||
| from .filter import Filter | ||||
| from .icalendarparser import ICalendarParser | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| OFFSET = "!!" | ||||
|  | ||||
|  | ||||
| MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) | ||||
|  | ||||
|  | ||||
| def setup_platform( | ||||
|     hass: HomeAssistant, | ||||
|     config: ConfigType, | ||||
|     add_entities: AddEntitiesCallback, | ||||
|     discovery_info: DiscoveryInfoType | None = None, | ||||
| ): | ||||
|     """Set up ics_calendar platform. | ||||
|  | ||||
|     :param hass: Home Assistant object | ||||
|     :type hass: HomeAssistant | ||||
|     :param config: Config information for the platform | ||||
|     :type config: ConfigType | ||||
|     :param add_entities: Callback to add entities to HA | ||||
|     :type add_entities: AddEntitiesCallback | ||||
|     :param discovery_info: Config information for the platform | ||||
|     :type discovery_info: DiscoveryInfoType | None, optional | ||||
|     """ | ||||
|     _LOGGER.debug("Setting up ics calendars") | ||||
|     if discovery_info is not None: | ||||
|         calendars: list = discovery_info.get(CONF_CALENDARS) | ||||
|     else: | ||||
|         calendars: list = config.get(CONF_CALENDARS) | ||||
|  | ||||
|     calendar_devices = [] | ||||
|     for calendar in calendars: | ||||
|         device_data = { | ||||
|             CONF_NAME: calendar.get(CONF_NAME), | ||||
|             CONF_URL: calendar.get(CONF_URL), | ||||
|             CONF_INCLUDE_ALL_DAY: calendar.get(CONF_INCLUDE_ALL_DAY), | ||||
|             CONF_USERNAME: calendar.get(CONF_USERNAME), | ||||
|             CONF_PASSWORD: calendar.get(CONF_PASSWORD), | ||||
|             CONF_PARSER: calendar.get(CONF_PARSER), | ||||
|             CONF_PREFIX: calendar.get(CONF_PREFIX), | ||||
|             CONF_DAYS: calendar.get(CONF_DAYS), | ||||
|             CONF_DOWNLOAD_INTERVAL: calendar.get(CONF_DOWNLOAD_INTERVAL), | ||||
|             CONF_USER_AGENT: calendar.get(CONF_USER_AGENT), | ||||
|             CONF_EXCLUDE: calendar.get(CONF_EXCLUDE), | ||||
|             CONF_INCLUDE: calendar.get(CONF_INCLUDE), | ||||
|             CONF_OFFSET_HOURS: calendar.get(CONF_OFFSET_HOURS), | ||||
|             CONF_ACCEPT_HEADER: calendar.get(CONF_ACCEPT_HEADER), | ||||
|         } | ||||
|         device_id = f"{device_data[CONF_NAME]}" | ||||
|         entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass) | ||||
|         calendar_devices.append(ICSCalendarEntity(entity_id, device_data)) | ||||
|  | ||||
|     add_entities(calendar_devices) | ||||
|  | ||||
|  | ||||
| class ICSCalendarEntity(CalendarEntity): | ||||
|     """A CalendarEntity for an ICS Calendar.""" | ||||
|  | ||||
|     def __init__(self, entity_id: str, device_data): | ||||
|         """Construct ICSCalendarEntity. | ||||
|  | ||||
|         :param entity_id: Entity id for the calendar | ||||
|         :type entity_id: str | ||||
|         :param device_data: dict describing the calendar | ||||
|         :type device_data: dict | ||||
|         """ | ||||
|         _LOGGER.debug( | ||||
|             "Initializing calendar: %s with URL: %s", | ||||
|             device_data[CONF_NAME], | ||||
|             device_data[CONF_URL], | ||||
|         ) | ||||
|         self.data = ICSCalendarData(device_data) | ||||
|         self.entity_id = entity_id | ||||
|         self._event = None | ||||
|         self._name = device_data[CONF_NAME] | ||||
|         self._last_call = None | ||||
|  | ||||
|     @property | ||||
|     def event(self) -> Optional[CalendarEvent]: | ||||
|         """Return the current or next upcoming event or None. | ||||
|  | ||||
|         :return: The current event as a dict | ||||
|         :rtype: dict | ||||
|         """ | ||||
|         return self._event | ||||
|  | ||||
|     @property | ||||
|     def name(self): | ||||
|         """Return the name of the calendar.""" | ||||
|         return self._name | ||||
|  | ||||
|     @property | ||||
|     def should_poll(self): | ||||
|         """Indicate if the calendar should be polled. | ||||
|  | ||||
|         If the last call to update or get_api_events was not within the minimum | ||||
|         update time, then async_schedule_update_ha_state(True) is also called. | ||||
|         :return: True | ||||
|         :rtype: boolean | ||||
|         """ | ||||
|         this_call = hanow() | ||||
|         if ( | ||||
|             self._last_call is None | ||||
|             or (this_call - self._last_call) > MIN_TIME_BETWEEN_UPDATES | ||||
|         ): | ||||
|             self._last_call = this_call | ||||
|             self.async_schedule_update_ha_state(True) | ||||
|         return True | ||||
|  | ||||
|     async def async_get_events( | ||||
|         self, hass: HomeAssistant, start_date: datetime, end_date: datetime | ||||
|     ) -> list[CalendarEvent]: | ||||
|         """Get all events in a specific time frame. | ||||
|  | ||||
|         :param hass: Home Assistant object | ||||
|         :type hass: HomeAssistant | ||||
|         :param start_date: The first starting date to consider | ||||
|         :type start_date: datetime | ||||
|         :param end_date: The last starting date to consider | ||||
|         :type end_date: datetime | ||||
|         """ | ||||
|         _LOGGER.debug( | ||||
|             "%s: async_get_events called; calling internal.", self.name | ||||
|         ) | ||||
|         return await self.data.async_get_events(hass, start_date, end_date) | ||||
|  | ||||
|     def update(self): | ||||
|         """Get the current or next event.""" | ||||
|         self.data.update() | ||||
|         self._event = self.data.event | ||||
|         self._attr_extra_state_attributes = { | ||||
|             "offset_reached": is_offset_reached( | ||||
|                 self._event.start_datetime_local, self.data.offset | ||||
|             ) | ||||
|             if self._event | ||||
|             else False | ||||
|         } | ||||
|  | ||||
|  | ||||
| class ICSCalendarData:  # pylint: disable=R0902 | ||||
|     """Class to use the calendar ICS client object to get next event.""" | ||||
|  | ||||
|     def __init__(self, device_data): | ||||
|         """Set up how we are going to connect to the URL. | ||||
|  | ||||
|         :param device_data Information about the calendar | ||||
|         """ | ||||
|         self.name = device_data[CONF_NAME] | ||||
|         self._days = device_data[CONF_DAYS] | ||||
|         self._offset_hours = device_data[CONF_OFFSET_HOURS] | ||||
|         self.include_all_day = device_data[CONF_INCLUDE_ALL_DAY] | ||||
|         self._summary_prefix: str = device_data[CONF_PREFIX] | ||||
|         self.parser = ICalendarParser.get_instance(device_data[CONF_PARSER]) | ||||
|         self.parser.set_filter( | ||||
|             Filter(device_data[CONF_EXCLUDE], device_data[CONF_INCLUDE]) | ||||
|         ) | ||||
|         self.offset = None | ||||
|         self.event = None | ||||
|  | ||||
|         self._calendar_data = CalendarData( | ||||
|             _LOGGER, | ||||
|             self.name, | ||||
|             device_data[CONF_URL], | ||||
|             timedelta(minutes=device_data[CONF_DOWNLOAD_INTERVAL]), | ||||
|         ) | ||||
|  | ||||
|         self._calendar_data.set_headers( | ||||
|             device_data[CONF_USERNAME], | ||||
|             device_data[CONF_PASSWORD], | ||||
|             device_data[CONF_USER_AGENT], | ||||
|             device_data[CONF_ACCEPT_HEADER], | ||||
|         ) | ||||
|  | ||||
|     async def async_get_events( | ||||
|         self, hass: HomeAssistant, start_date: datetime, end_date: datetime | ||||
|     ) -> list[CalendarEvent]: | ||||
|         """Get all events in a specific time frame. | ||||
|  | ||||
|         :param hass: Home Assistant object | ||||
|         :type hass: HomeAssistant | ||||
|         :param start_date: The first starting date to consider | ||||
|         :type start_date: datetime | ||||
|         :param end_date: The last starting date to consider | ||||
|         :type end_date: datetime | ||||
|         """ | ||||
|         event_list = [] | ||||
|         if await hass.async_add_executor_job( | ||||
|             self._calendar_data.download_calendar | ||||
|         ): | ||||
|             _LOGGER.debug("%s: Setting calendar content", self.name) | ||||
|             self.parser.set_content(self._calendar_data.get()) | ||||
|         try: | ||||
|             event_list = self.parser.get_event_list( | ||||
|                 start=start_date, | ||||
|                 end=end_date, | ||||
|                 include_all_day=self.include_all_day, | ||||
|                 offset_hours=self._offset_hours, | ||||
|             ) | ||||
|         except:  # pylint: disable=W0702 | ||||
|             _LOGGER.error( | ||||
|                 "async_get_events: %s: Failed to parse ICS!", | ||||
|                 self.name, | ||||
|                 exc_info=True, | ||||
|             ) | ||||
|             event_list = [] | ||||
|  | ||||
|         for event in event_list: | ||||
|             event.summary = self._summary_prefix + event.summary | ||||
|  | ||||
|         return event_list | ||||
|  | ||||
|     @Throttle(MIN_TIME_BETWEEN_UPDATES) | ||||
|     def update(self): | ||||
|         """Get the current or next event.""" | ||||
|         _LOGGER.debug("%s: Update was called", self.name) | ||||
|         if self._calendar_data.download_calendar(): | ||||
|             _LOGGER.debug("%s: Setting calendar content", self.name) | ||||
|             self.parser.set_content(self._calendar_data.get()) | ||||
|         try: | ||||
|             self.event = self.parser.get_current_event( | ||||
|                 include_all_day=self.include_all_day, | ||||
|                 now=hanow(), | ||||
|                 days=self._days, | ||||
|                 offset_hours=self._offset_hours, | ||||
|             ) | ||||
|         except:  # pylint: disable=W0702 | ||||
|             _LOGGER.error( | ||||
|                 "update: %s: Failed to parse ICS!", self.name, exc_info=True | ||||
|             ) | ||||
|         if self.event is not None: | ||||
|             _LOGGER.debug( | ||||
|                 "%s: got event: %s; start: %s; end: %s; all_day: %s", | ||||
|                 self.name, | ||||
|                 self.event.summary, | ||||
|                 self.event.start, | ||||
|                 self.event.end, | ||||
|                 self.event.all_day, | ||||
|             ) | ||||
|             (summary, offset) = extract_offset(self.event.summary, OFFSET) | ||||
|             self.event.summary = self._summary_prefix + summary | ||||
|             self.offset = offset | ||||
|             return True | ||||
|  | ||||
|         _LOGGER.debug("%s: No event found!", self.name) | ||||
|         return False | ||||
							
								
								
									
										198
									
								
								custom_components/ics_calendar/calendardata.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								custom_components/ics_calendar/calendardata.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,198 @@ | ||||
| """Provide CalendarData class.""" | ||||
| import zlib | ||||
| from datetime import timedelta | ||||
| from gzip import BadGzipFile, GzipFile | ||||
| from logging import Logger | ||||
| from threading import Lock | ||||
| from urllib.error import ContentTooShortError, HTTPError, URLError | ||||
| from urllib.request import ( | ||||
|     HTTPBasicAuthHandler, | ||||
|     HTTPDigestAuthHandler, | ||||
|     HTTPPasswordMgrWithDefaultRealm, | ||||
|     build_opener, | ||||
|     install_opener, | ||||
|     urlopen, | ||||
| ) | ||||
|  | ||||
| from homeassistant.util.dt import now as hanow | ||||
|  | ||||
|  | ||||
| class CalendarData: | ||||
|     """CalendarData class. | ||||
|  | ||||
|     The CalendarData class is used to download and cache calendar data from a | ||||
|     given URL.  Use the get method to retrieve the data after constructing your | ||||
|     instance. | ||||
|     """ | ||||
|  | ||||
|     opener_lock = Lock() | ||||
|  | ||||
|     def __init__( | ||||
|         self, logger: Logger, name: str, url: str, min_update_time: timedelta | ||||
|     ): | ||||
|         """Construct CalendarData object. | ||||
|  | ||||
|         :param logger: The logger for reporting problems | ||||
|         :type logger: Logger | ||||
|         :param name: The name of the calendar (used for reporting problems) | ||||
|         :type name: str | ||||
|         :param url: The URL of the calendar | ||||
|         :type url: str | ||||
|         :param min_update_time: The minimum time between downloading data from | ||||
|             the URL when requested | ||||
|         :type min_update_time: timedelta | ||||
|         """ | ||||
|         self._calendar_data = None | ||||
|         self._last_download = None | ||||
|         self._min_update_time = min_update_time | ||||
|         self._opener = None | ||||
|         self.logger = logger | ||||
|         self.name = name | ||||
|         self.url = url | ||||
|  | ||||
|     def download_calendar(self) -> bool: | ||||
|         """Download the calendar data. | ||||
|  | ||||
|         This only downloads data if self.min_update_time has passed since the | ||||
|         last download. | ||||
|  | ||||
|         returns: True if data was downloaded, otherwise False. | ||||
|         rtype: bool | ||||
|         """ | ||||
|         now = hanow() | ||||
|         if ( | ||||
|             self._calendar_data is None | ||||
|             or self._last_download is None | ||||
|             or (now - self._last_download) > self._min_update_time | ||||
|         ): | ||||
|             self._last_download = now | ||||
|             self._calendar_data = None | ||||
|             self.logger.debug( | ||||
|                 "%s: Downloading calendar data from: %s", self.name, self.url | ||||
|             ) | ||||
|             self._download_data() | ||||
|             return self._calendar_data is not None | ||||
|  | ||||
|         return False | ||||
|  | ||||
|     def get(self) -> str: | ||||
|         """Get the calendar data that was downloaded. | ||||
|  | ||||
|         :return: The downloaded calendar data. | ||||
|         :rtype: str | ||||
|         """ | ||||
|         return self._calendar_data | ||||
|  | ||||
|     def set_headers( | ||||
|         self, | ||||
|         user_name: str, | ||||
|         password: str, | ||||
|         user_agent: str, | ||||
|         accept_header: str, | ||||
|     ): | ||||
|         """Set a user agent, accept header, and/or user name and password. | ||||
|  | ||||
|         The user name and password will be set into an HTTPBasicAuthHandler an | ||||
|         an HTTPDigestAuthHandler.  Both are attached to a new urlopener, so | ||||
|         that HTTP Basic Auth and HTTP Digest Auth will be supported when | ||||
|         opening the URL. | ||||
|  | ||||
|         If the user_agent parameter is not "", a User-agent header will be | ||||
|         added to the urlopener. | ||||
|  | ||||
|         :param user_name: The user name | ||||
|         :type user_name: str | ||||
|         :param password: The password | ||||
|         :type password: str | ||||
|         :param user_agent: The User Agent string to use or "" | ||||
|         :type user_agent: str | ||||
|         :param accept_header: The accept header string to use or "" | ||||
|         :type accept_header: str | ||||
|         """ | ||||
|         if user_name != "" and password != "": | ||||
|             passman = HTTPPasswordMgrWithDefaultRealm() | ||||
|             passman.add_password(None, self.url, user_name, password) | ||||
|             basic_auth_handler = HTTPBasicAuthHandler(passman) | ||||
|             digest_auth_handler = HTTPDigestAuthHandler(passman) | ||||
|             self._opener = build_opener( | ||||
|                 digest_auth_handler, basic_auth_handler | ||||
|             ) | ||||
|  | ||||
|         additional_headers = [] | ||||
|         if user_agent != "": | ||||
|             additional_headers.append(("User-agent", user_agent)) | ||||
|         if accept_header != "": | ||||
|             additional_headers.append(("Accept", accept_header)) | ||||
|         if len(additional_headers) > 0: | ||||
|             if self._opener is None: | ||||
|                 self._opener = build_opener() | ||||
|             self._opener.addheaders = additional_headers | ||||
|  | ||||
|     def _decode_data(self, conn): | ||||
|         if ( | ||||
|             "Content-Encoding" in conn.headers | ||||
|             and conn.headers["Content-Encoding"] == "gzip" | ||||
|         ): | ||||
|             reader = GzipFile(fileobj=conn) | ||||
|         else: | ||||
|             reader = conn | ||||
|         try: | ||||
|             return self._decode_stream(reader.read()).replace("\0", "") | ||||
|         except zlib.error: | ||||
|             self.logger.error( | ||||
|                 "%s: Failed to uncompress gzip data from url(%s): zlib", | ||||
|                 self.name, | ||||
|                 self.url, | ||||
|             ) | ||||
|         except BadGzipFile as gzip_error: | ||||
|             self.logger.error( | ||||
|                 "%s: Failed to uncompress gzip data from url(%s): %s", | ||||
|                 self.name, | ||||
|                 self.url, | ||||
|                 gzip_error.strerror, | ||||
|             ) | ||||
|         return None | ||||
|  | ||||
|     def _decode_stream(self, strm): | ||||
|         for encoding in "utf-8-sig", "utf-8", "utf-16": | ||||
|             try: | ||||
|                 return strm.decode(encoding) | ||||
|             except UnicodeDecodeError: | ||||
|                 continue | ||||
|         return None | ||||
|  | ||||
|     def _download_data(self): | ||||
|         """Download the calendar data.""" | ||||
|         try: | ||||
|             with CalendarData.opener_lock: | ||||
|                 if self._opener is not None: | ||||
|                     install_opener(self._opener) | ||||
|                 with urlopen(self._make_url()) as conn: | ||||
|                     self._calendar_data = self._decode_data(conn) | ||||
|         except HTTPError as http_error: | ||||
|             self.logger.error( | ||||
|                 "%s: Failed to open url(%s): %s", | ||||
|                 self.name, | ||||
|                 self.url, | ||||
|                 http_error.reason, | ||||
|             ) | ||||
|         except ContentTooShortError as content_too_short_error: | ||||
|             self.logger.error( | ||||
|                 "%s: Could not download calendar data: %s", | ||||
|                 self.name, | ||||
|                 content_too_short_error.reason, | ||||
|             ) | ||||
|         except URLError as url_error: | ||||
|             self.logger.error( | ||||
|                 "%s: Failed to open url: %s", self.name, url_error.reason | ||||
|             ) | ||||
|         except:  # pylint: disable=W0702 | ||||
|             self.logger.error( | ||||
|                 "%s: Failed to open url!", self.name, exc_info=True | ||||
|             ) | ||||
|  | ||||
|     def _make_url(self): | ||||
|         now = hanow() | ||||
|         return self.url.replace("{year}", f"{now.year:04}").replace( | ||||
|             "{month}", f"{now.month:02}" | ||||
|         ) | ||||
							
								
								
									
										7
									
								
								custom_components/ics_calendar/const.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								custom_components/ics_calendar/const.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| """Constants for ics_calendar platform.""" | ||||
| VERSION = "4.1.0" | ||||
| DOMAIN = "ics_calendar" | ||||
| UPGRADE_URL = ( | ||||
|     "https://github.com/franc6/ics_calendar/blob/releases/" | ||||
|     "UpgradeTo4.0AndLater.md" | ||||
| ) | ||||
							
								
								
									
										124
									
								
								custom_components/ics_calendar/filter.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								custom_components/ics_calendar/filter.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
| """Provide Filter class.""" | ||||
| import re | ||||
| from ast import literal_eval | ||||
| from typing import List, Optional, Pattern | ||||
|  | ||||
| from homeassistant.components.calendar import CalendarEvent | ||||
|  | ||||
|  | ||||
| class Filter: | ||||
|     """Filter class. | ||||
|  | ||||
|     The Filter class is used to filter events according to the exclude and | ||||
|     include rules. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, exclude: str, include: str): | ||||
|         """Construct Filter class. | ||||
|  | ||||
|         :param exclude: The exclude rules | ||||
|         :type exclude: str | ||||
|         :param include: The include rules | ||||
|         :type include: str | ||||
|         """ | ||||
|         self._exclude = Filter.set_rules(exclude) | ||||
|         self._include = Filter.set_rules(include) | ||||
|  | ||||
|     @staticmethod | ||||
|     def set_rules(rules: str) -> List[Pattern]: | ||||
|         """Set the given rules into an array which is returned. | ||||
|  | ||||
|         :param rules: The rules to set | ||||
|         :type rules: str | ||||
|         :return: An array of regular expressions | ||||
|         :rtype: List[Pattern] | ||||
|         """ | ||||
|         arr = [] | ||||
|         if rules != "": | ||||
|             for rule in literal_eval(rules): | ||||
|                 if rule.startswith("/"): | ||||
|                     re_flags = re.NOFLAG | ||||
|                     [expr, flags] = rule[1:].split("/") | ||||
|                     for flag in flags: | ||||
|                         match flag: | ||||
|                             case "i": | ||||
|                                 re_flags |= re.IGNORECASE | ||||
|                             case "m": | ||||
|                                 re_flags |= re.MULTILINE | ||||
|                             case "s": | ||||
|                                 re_flags |= re.DOTALL | ||||
|                     arr.append(re.compile(expr, re_flags)) | ||||
|                 else: | ||||
|                     arr.append(re.compile(rule, re.IGNORECASE)) | ||||
|         return arr | ||||
|  | ||||
|     def _is_match( | ||||
|         self, summary: str, description: Optional[str], regexes: List[Pattern] | ||||
|     ) -> bool: | ||||
|         """Indicate if the event matches the given list of regular expressions. | ||||
|  | ||||
|         :param summary: The event summary to examine | ||||
|         :type summary: str | ||||
|         :param description: The event description summary to examine | ||||
|         :type description: Optional[str] | ||||
|         :param regexes: The regular expressions to match against | ||||
|         :type regexes: List[] | ||||
|         :return: True if the event matches the exclude filter | ||||
|         :rtype: bool | ||||
|         """ | ||||
|         for regex in regexes: | ||||
|             if regex.search(summary) or ( | ||||
|                 description and regex.search(description) | ||||
|             ): | ||||
|                 return True | ||||
|  | ||||
|         return False | ||||
|  | ||||
|     def _is_excluded(self, summary: str, description: Optional[str]) -> bool: | ||||
|         """Indicate if the event should be excluded. | ||||
|  | ||||
|         :param summary: The event summary to examine | ||||
|         :type summary: str | ||||
|         :param description: The event description summary to examine | ||||
|         :type description: Optional[str] | ||||
|         :return: True if the event matches the exclude filter | ||||
|         :rtype: bool | ||||
|         """ | ||||
|         return self._is_match(summary, description, self._exclude) | ||||
|  | ||||
|     def _is_included(self, summary: str, description: Optional[str]) -> bool: | ||||
|         """Indicate if the event should be included. | ||||
|  | ||||
|         :param summary: The event summary to examine | ||||
|         :type summary: str | ||||
|         :param description: The event description summary to examine | ||||
|         :type description: Optional[str] | ||||
|         :return: True if the event matches the include filter | ||||
|         :rtype: bool | ||||
|         """ | ||||
|         return self._is_match(summary, description, self._include) | ||||
|  | ||||
|     def filter(self, summary: str, description: Optional[str]) -> bool: | ||||
|         """Check if the event should be included or not. | ||||
|  | ||||
|         :param summary: The event summary to examine | ||||
|         :type summary: str | ||||
|         :param description: The event description summary to examine | ||||
|         :type description: Optional[str] | ||||
|         :return: true if the event should be included, otherwise false | ||||
|         :rtype: bool | ||||
|         """ | ||||
|         add_event = not self._is_excluded(summary, description) | ||||
|         if not add_event: | ||||
|             add_event = self._is_included(summary, description) | ||||
|         return add_event | ||||
|  | ||||
|     def filter_event(self, event: CalendarEvent) -> bool: | ||||
|         """Check if the event should be included or not. | ||||
|  | ||||
|         :param event: The event to examine | ||||
|         :type event: CalendarEvent | ||||
|         :return: true if the event should be included, otherwise false | ||||
|         :rtype: bool | ||||
|         """ | ||||
|         return self.filter(event.summary, event.description) | ||||
							
								
								
									
										97
									
								
								custom_components/ics_calendar/icalendarparser.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								custom_components/ics_calendar/icalendarparser.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| """Provide ICalendarParser class.""" | ||||
| import importlib | ||||
| from datetime import datetime | ||||
| from typing import Optional | ||||
|  | ||||
| from homeassistant.components.calendar import CalendarEvent | ||||
|  | ||||
| from .filter import Filter | ||||
|  | ||||
|  | ||||
| class ICalendarParser: | ||||
|     """Provide interface for various parser classes. | ||||
|  | ||||
|     The class provides a static method , get_instace, to get a parser instance. | ||||
|     The non static methods allow this class to act as an "interface" for the | ||||
|     parser classes. | ||||
|     """ | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_class(parser: str): | ||||
|         """Get the class of the requested parser.""" | ||||
|         parser_module_name = ".parsers.parser_" + parser | ||||
|         parser = "Parser" + parser.upper() | ||||
|         try: | ||||
|             module = importlib.import_module(parser_module_name, __package__) | ||||
|             return getattr(module, parser) | ||||
|         except ImportError: | ||||
|             return None | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_instance(parser: str, *args): | ||||
|         """Get an instance of the requested parser.""" | ||||
|         parser_cls = ICalendarParser.get_class(parser) | ||||
|         if parser_cls is not None: | ||||
|             return parser_cls(*args) | ||||
|         return None | ||||
|  | ||||
|     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 | ||||
|         """ | ||||
|  | ||||
|     def set_filter(self, filt: Filter): | ||||
|         """Set a Filter object to filter events. | ||||
|  | ||||
|         :param filt: The Filter object | ||||
|         :type exclude: Filter | ||||
|         """ | ||||
|  | ||||
|     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 start datetime | ||||
|         :param end the latest start time of events to return | ||||
|         :type end datetime | ||||
|         :param include_all_day if true, all day events will be included. | ||||
|         :type include_all_day 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] | ||||
|         """ | ||||
|  | ||||
|     def get_current_event( | ||||
|         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 include_all_day boolean | ||||
|         :param now the current date and time | ||||
|         :type now datetime | ||||
|         :param days the number of days to check for an upcoming event | ||||
|         :type days int | ||||
|         :param offset_hours the number of hours to offset the event | ||||
|         :type offset_hours int | ||||
|         :returns a CalendarEvent or None | ||||
|         """ | ||||
							
								
								
									
										13
									
								
								custom_components/ics_calendar/manifest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								custom_components/ics_calendar/manifest.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| { | ||||
|  | ||||
|     "domain": "ics_calendar", | ||||
|     "name": "ics Calendar", | ||||
|     "codeowners": ["@franc6"], | ||||
|     "dependencies": [], | ||||
|     "documentation": "https://github.com/franc6/ics_calendar", | ||||
|     "integration_type": "service", | ||||
|     "iot_class": "cloud_polling", | ||||
|     "issue_tracker": "https://github.com/franc6/ics_calendar/issues", | ||||
|     "requirements": ["ics>=0.7.2", "recurring_ical_events>=2.0.2", "icalendar>=5.0.4"], | ||||
|     "version": "4.1.0" | ||||
| } | ||||
							
								
								
									
										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 | ||||
							
								
								
									
										37
									
								
								custom_components/ics_calendar/utility.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								custom_components/ics_calendar/utility.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| """Utility methods.""" | ||||
| from datetime import date, datetime | ||||
|  | ||||
|  | ||||
| def make_datetime(val): | ||||
|     """Ensure val is a datetime, not a date.""" | ||||
|     if isinstance(val, date) and not isinstance(val, datetime): | ||||
|         return datetime.combine(val, datetime.min.time()).astimezone() | ||||
|     return val | ||||
|  | ||||
|  | ||||
| def compare_event_dates(  # pylint: disable=R0913 | ||||
|     now, end2, start2, all_day2, end, start, all_day | ||||
| ) -> bool: | ||||
|     """Determine if end2 and start2 are newer than end and start.""" | ||||
|     # Make sure we only compare datetime values, not dates with datetimes. | ||||
|     # Set each date object to a datetime at midnight. | ||||
|     end = make_datetime(end) | ||||
|     end2 = make_datetime(end2) | ||||
|     start = make_datetime(start) | ||||
|     start2 = make_datetime(start2) | ||||
|  | ||||
|     if all_day2 == all_day: | ||||
|         if end2 == end: | ||||
|             return start2 > start | ||||
|         return end2 > end and start2 >= start | ||||
|  | ||||
|     if now.tzinfo is None: | ||||
|         now = now.astimezone() | ||||
|  | ||||
|     event2_current = start2 <= now <= end2 | ||||
|     event_current = start <= now <= end | ||||
|  | ||||
|     if event_current and event2_current: | ||||
|         return all_day | ||||
|  | ||||
|     return start2 >= start or end2 >= end | ||||
		Reference in New Issue
	
	Block a user