Added ics_calender custom component.
This commit is contained in:
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}"
|
||||
)
|
Reference in New Issue
Block a user