Updated ics_calendar to restore compatibility with HA
This commit is contained in:
parent
fef90d5a78
commit
ca599eab7a
@ -42,7 +42,6 @@ template: !include template.yaml
|
||||
|
||||
# calendar integration
|
||||
calendar: !include calendars.yaml
|
||||
ics_calendar: !include ics_calendars.yaml
|
||||
|
||||
# DB-recorder configuration
|
||||
recorder: !include recorder.yaml
|
||||
|
@ -4,6 +4,7 @@ import logging
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import voluptuous as vol
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_EXCLUDE,
|
||||
CONF_INCLUDE,
|
||||
@ -14,24 +15,35 @@ from homeassistant.const import (
|
||||
CONF_USERNAME,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, UPGRADE_URL
|
||||
from .const import (
|
||||
CONF_ACCEPT_HEADER,
|
||||
CONF_ADV_CONNECT_OPTS,
|
||||
CONF_CALENDARS,
|
||||
CONF_CONNECTION_TIMEOUT,
|
||||
CONF_DAYS,
|
||||
CONF_DOWNLOAD_INTERVAL,
|
||||
CONF_INCLUDE_ALL_DAY,
|
||||
CONF_OFFSET_HOURS,
|
||||
CONF_PARSER,
|
||||
CONF_REQUIRES_AUTH,
|
||||
CONF_SET_TIMEOUT,
|
||||
CONF_SUMMARY_DEFAULT,
|
||||
CONF_SUMMARY_DEFAULT_DEFAULT,
|
||||
CONF_USER_AGENT,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_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(
|
||||
@ -81,6 +93,13 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Optional(
|
||||
CONF_ACCEPT_HEADER, default=""
|
||||
): cv.string,
|
||||
vol.Optional(
|
||||
CONF_CONNECTION_TIMEOUT, default=300
|
||||
): cv.positive_float,
|
||||
vol.Optional(
|
||||
CONF_SUMMARY_DEFAULT,
|
||||
default=CONF_SUMMARY_DEFAULT_DEFAULT,
|
||||
): cv.string,
|
||||
}
|
||||
)
|
||||
]
|
||||
@ -92,22 +111,150 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
STORAGE_KEY = DOMAIN
|
||||
STORAGE_VERSION_MAJOR = 1
|
||||
STORAGE_VERSION_MINOR = 0
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
async def async_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
|
||||
_LOGGER.debug("discovery.load_platform called")
|
||||
discovery.load_platform(
|
||||
hass=hass,
|
||||
component=PLATFORMS[0],
|
||||
platform=DOMAIN,
|
||||
discovered=config[DOMAIN],
|
||||
hass_config=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,
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_yaml_configuration",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="YAML_Warning",
|
||||
)
|
||||
_LOGGER.warning(
|
||||
"YAML configuration of ics_calendar is deprecated and will be "
|
||||
"removed in ics_calendar v5.0.0. Your configuration items have "
|
||||
"been imported. Please remove them from your configuration.yaml "
|
||||
"file."
|
||||
)
|
||||
|
||||
config_entry = _async_find_matching_config_entry(hass)
|
||||
if not config_entry:
|
||||
if config[DOMAIN].get("calendars"):
|
||||
for calendar in config[DOMAIN].get("calendars"):
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=dict(calendar),
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
# update entry with any changes
|
||||
if config[DOMAIN].get("calendars"):
|
||||
for calendar in config[DOMAIN].get("calendars"):
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, data=dict(calendar)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@callback
|
||||
def _async_find_matching_config_entry(hass):
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
if entry.source == SOURCE_IMPORT:
|
||||
return entry
|
||||
return None
|
||||
|
||||
|
||||
async def async_migrate_entry(hass, entry: ConfigEntry):
|
||||
"""Migrate old config entry."""
|
||||
# Don't downgrade entries
|
||||
if entry.version > STORAGE_VERSION_MAJOR:
|
||||
return False
|
||||
|
||||
if entry.version == STORAGE_VERSION_MAJOR:
|
||||
new_data = {**entry.data}
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data=new_data,
|
||||
minor_version=STORAGE_VERSION_MINOR,
|
||||
version=STORAGE_VERSION_MAJOR,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Implement async_setup_entry."""
|
||||
full_data: dict = add_missing_defaults(entry)
|
||||
hass.config_entries.async_update_entry(entry=entry, data=full_data)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = full_data
|
||||
await hass.config_entries.async_forward_entry_setups(entry, ["calendar"])
|
||||
return True
|
||||
|
||||
|
||||
def add_missing_defaults(
|
||||
entry: ConfigEntry,
|
||||
) -> dict:
|
||||
"""Initialize missing data."""
|
||||
data = {
|
||||
CONF_NAME: "",
|
||||
CONF_URL: "",
|
||||
CONF_ADV_CONNECT_OPTS: False,
|
||||
CONF_SET_TIMEOUT: False,
|
||||
CONF_REQUIRES_AUTH: False,
|
||||
CONF_INCLUDE_ALL_DAY: False,
|
||||
CONF_REQUIRES_AUTH: False,
|
||||
CONF_USERNAME: "",
|
||||
CONF_PASSWORD: "",
|
||||
CONF_PARSER: "rie",
|
||||
CONF_PREFIX: "",
|
||||
CONF_DAYS: 1,
|
||||
CONF_DOWNLOAD_INTERVAL: 15,
|
||||
CONF_USER_AGENT: "",
|
||||
CONF_EXCLUDE: "",
|
||||
CONF_INCLUDE: "",
|
||||
CONF_OFFSET_HOURS: 0,
|
||||
CONF_ACCEPT_HEADER: "",
|
||||
CONF_CONNECTION_TIMEOUT: 300.0,
|
||||
CONF_SUMMARY_DEFAULT: CONF_SUMMARY_DEFAULT_DEFAULT,
|
||||
}
|
||||
data.update(entry.data)
|
||||
|
||||
if CONF_USERNAME in entry.data or CONF_PASSWORD in entry.data:
|
||||
data[CONF_REQUIRES_AUTH] = True
|
||||
if (
|
||||
CONF_USER_AGENT in entry.data
|
||||
or CONF_ACCEPT_HEADER in entry.data
|
||||
or CONF_CONNECTION_TIMEOUT in entry.data
|
||||
):
|
||||
data[CONF_ADV_CONNECT_OPTS] = True
|
||||
if CONF_CONNECTION_TIMEOUT in entry.data:
|
||||
data[CONF_SET_TIMEOUT] = True
|
||||
|
||||
return data
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
entry, PLATFORMS
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
@ -1,7 +1,8 @@
|
||||
"""Support for ICS Calendar."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
# import homeassistant.helpers.config_validation as cv
|
||||
# import voluptuous as vol
|
||||
@ -12,6 +13,7 @@ from homeassistant.components.calendar import (
|
||||
extract_offset,
|
||||
is_offset_reached,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_EXCLUDE,
|
||||
CONF_INCLUDE,
|
||||
@ -24,23 +26,28 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.util.dt import now as hanow
|
||||
|
||||
from . import (
|
||||
from .calendardata import CalendarData
|
||||
from .const import (
|
||||
CONF_ACCEPT_HEADER,
|
||||
CONF_CALENDARS,
|
||||
CONF_CONNECTION_TIMEOUT,
|
||||
CONF_DAYS,
|
||||
CONF_DOWNLOAD_INTERVAL,
|
||||
CONF_INCLUDE_ALL_DAY,
|
||||
CONF_OFFSET_HOURS,
|
||||
CONF_PARSER,
|
||||
CONF_SET_TIMEOUT,
|
||||
CONF_SUMMARY_DEFAULT,
|
||||
CONF_USER_AGENT,
|
||||
DOMAIN,
|
||||
)
|
||||
from .calendardata import CalendarData
|
||||
from .filter import Filter
|
||||
from .icalendarparser import ICalendarParser
|
||||
from .getparser import GetParser
|
||||
from .parserevent import ParserEvent
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -51,6 +58,34 @@ OFFSET = "!!"
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the calendar in background."""
|
||||
hass.async_create_task(
|
||||
_async_setup_entry_bg_task(hass, config_entry, async_add_entities)
|
||||
)
|
||||
|
||||
|
||||
async def _async_setup_entry_bg_task(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the calendar."""
|
||||
data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
device_id = f"{data[CONF_NAME]}"
|
||||
entity = ICSCalendarEntity(
|
||||
hass,
|
||||
generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass),
|
||||
hass.data[DOMAIN][config_entry.entry_id],
|
||||
config_entry.entry_id,
|
||||
)
|
||||
async_add_entities([entity])
|
||||
|
||||
|
||||
def setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
@ -70,8 +105,13 @@ def setup_platform(
|
||||
"""
|
||||
_LOGGER.debug("Setting up ics calendars")
|
||||
if discovery_info is not None:
|
||||
calendars: list = discovery_info.get(CONF_CALENDARS)
|
||||
_LOGGER.debug(
|
||||
"setup_platform: ignoring discovery_info, already imported!"
|
||||
)
|
||||
# calendars: list = discovery_info.get(CONF_CALENDARS)
|
||||
calendars = []
|
||||
else:
|
||||
_LOGGER.debug("setup_platform: discovery_info is None")
|
||||
calendars: list = config.get(CONF_CALENDARS)
|
||||
|
||||
calendar_devices = []
|
||||
@ -91,10 +131,13 @@ def setup_platform(
|
||||
CONF_INCLUDE: calendar.get(CONF_INCLUDE),
|
||||
CONF_OFFSET_HOURS: calendar.get(CONF_OFFSET_HOURS),
|
||||
CONF_ACCEPT_HEADER: calendar.get(CONF_ACCEPT_HEADER),
|
||||
CONF_CONNECTION_TIMEOUT: calendar.get(CONF_CONNECTION_TIMEOUT),
|
||||
}
|
||||
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))
|
||||
calendar_devices.append(
|
||||
ICSCalendarEntity(hass, entity_id, device_data)
|
||||
)
|
||||
|
||||
add_entities(calendar_devices)
|
||||
|
||||
@ -102,7 +145,13 @@ def setup_platform(
|
||||
class ICSCalendarEntity(CalendarEntity):
|
||||
"""A CalendarEntity for an ICS Calendar."""
|
||||
|
||||
def __init__(self, entity_id: str, device_data):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entity_id: str,
|
||||
device_data,
|
||||
unique_id: str = None,
|
||||
):
|
||||
"""Construct ICSCalendarEntity.
|
||||
|
||||
:param entity_id: Entity id for the calendar
|
||||
@ -111,14 +160,16 @@ class ICSCalendarEntity(CalendarEntity):
|
||||
:type device_data: dict
|
||||
"""
|
||||
_LOGGER.debug(
|
||||
"Initializing calendar: %s with URL: %s",
|
||||
"Initializing calendar: %s with URL: %s, uniqueid: %s",
|
||||
device_data[CONF_NAME],
|
||||
device_data[CONF_URL],
|
||||
unique_id,
|
||||
)
|
||||
self.data = ICSCalendarData(device_data)
|
||||
self.data = ICSCalendarData(hass, device_data)
|
||||
self.entity_id = entity_id
|
||||
self._attr_unique_id = f"ICSCalendar.{unique_id}"
|
||||
self._event = None
|
||||
self._name = device_data[CONF_NAME]
|
||||
self._attr_name = device_data[CONF_NAME]
|
||||
self._last_call = None
|
||||
|
||||
@property
|
||||
@ -131,12 +182,7 @@ class ICSCalendarEntity(CalendarEntity):
|
||||
return self._event
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the calendar."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
def should_poll(self) -> bool:
|
||||
"""Indicate if the calendar should be polled.
|
||||
|
||||
If the last call to update or get_api_events was not within the minimum
|
||||
@ -168,25 +214,50 @@ class ICSCalendarEntity(CalendarEntity):
|
||||
_LOGGER.debug(
|
||||
"%s: async_get_events called; calling internal.", self.name
|
||||
)
|
||||
return await self.data.async_get_events(hass, start_date, end_date)
|
||||
return await self.data.async_get_events(start_date, end_date)
|
||||
|
||||
def update(self):
|
||||
async def async_update(self):
|
||||
"""Get the current or next event."""
|
||||
self.data.update()
|
||||
self._event = self.data.event
|
||||
await self.data.async_update()
|
||||
self._event: CalendarEvent | None = self.data.event
|
||||
self._attr_extra_state_attributes = {
|
||||
"offset_reached": is_offset_reached(
|
||||
self._event.start_datetime_local, self.data.offset
|
||||
"offset_reached": (
|
||||
is_offset_reached(
|
||||
self._event.start_datetime_local, self.data.offset
|
||||
)
|
||||
if self._event
|
||||
else False
|
||||
)
|
||||
if self._event
|
||||
else False
|
||||
}
|
||||
|
||||
async def async_create_event(self, **kwargs: Any):
|
||||
"""Raise error, this is a read-only calendar."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_delete_event(
|
||||
self,
|
||||
uid: str,
|
||||
recurrence_id: str | None = None,
|
||||
recurrence_range: str | None = None,
|
||||
) -> None:
|
||||
"""Raise error, this is a read-only calendar."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_update_event(
|
||||
self,
|
||||
uid: str,
|
||||
event: dict[str, Any],
|
||||
recurrence_id: str | None = None,
|
||||
recurrence_range: str | None = None,
|
||||
) -> None:
|
||||
"""Raise error, this is a read-only calendar."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ICSCalendarData: # pylint: disable=R0902
|
||||
"""Class to use the calendar ICS client object to get next event."""
|
||||
|
||||
def __init__(self, device_data):
|
||||
def __init__(self, hass: HomeAssistant, device_data):
|
||||
"""Set up how we are going to connect to the URL.
|
||||
|
||||
:param device_data Information about the calendar
|
||||
@ -196,18 +267,25 @@ class ICSCalendarData: # pylint: disable=R0902
|
||||
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._summary_default: str = device_data[CONF_SUMMARY_DEFAULT]
|
||||
self.parser = GetParser.get_parser(device_data[CONF_PARSER])
|
||||
self.parser.set_filter(
|
||||
Filter(device_data[CONF_EXCLUDE], device_data[CONF_INCLUDE])
|
||||
)
|
||||
self.offset = None
|
||||
self.event = None
|
||||
self._hass = hass
|
||||
|
||||
self._calendar_data = CalendarData(
|
||||
get_async_client(hass),
|
||||
_LOGGER,
|
||||
self.name,
|
||||
device_data[CONF_URL],
|
||||
timedelta(minutes=device_data[CONF_DOWNLOAD_INTERVAL]),
|
||||
{
|
||||
"name": self.name,
|
||||
"url": device_data[CONF_URL],
|
||||
"min_update_time": timedelta(
|
||||
minutes=device_data[CONF_DOWNLOAD_INTERVAL]
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
self._calendar_data.set_headers(
|
||||
@ -217,22 +295,23 @@ class ICSCalendarData: # pylint: disable=R0902
|
||||
device_data[CONF_ACCEPT_HEADER],
|
||||
)
|
||||
|
||||
if device_data.get(CONF_SET_TIMEOUT):
|
||||
self._calendar_data.set_timeout(
|
||||
device_data[CONF_CONNECTION_TIMEOUT]
|
||||
)
|
||||
|
||||
async def async_get_events(
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
self, 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
|
||||
):
|
||||
event_list: list[ParserEvent] = []
|
||||
if await self._calendar_data.download_calendar():
|
||||
_LOGGER.debug("%s: Setting calendar content", self.name)
|
||||
self.parser.set_content(self._calendar_data.get())
|
||||
try:
|
||||
@ -248,22 +327,27 @@ class ICSCalendarData: # pylint: disable=R0902
|
||||
self.name,
|
||||
exc_info=True,
|
||||
)
|
||||
event_list = []
|
||||
event_list: list[ParserEvent] = []
|
||||
|
||||
for event in event_list:
|
||||
event.summary = self._summary_prefix + event.summary
|
||||
if not event.summary:
|
||||
event.summary = self._summary_default
|
||||
# Since we skipped the validation code earlier, invoke it now,
|
||||
# before passing the object outside this component
|
||||
event.validate()
|
||||
|
||||
return event_list
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
async def async_update(self):
|
||||
"""Get the current or next event."""
|
||||
_LOGGER.debug("%s: Update was called", self.name)
|
||||
if self._calendar_data.download_calendar():
|
||||
parser_event: ParserEvent | None = None
|
||||
if await 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(
|
||||
parser_event: ParserEvent | None = self.parser.get_current_event(
|
||||
include_all_day=self.include_all_day,
|
||||
now=hanow(),
|
||||
days=self._days,
|
||||
@ -273,18 +357,24 @@ class ICSCalendarData: # pylint: disable=R0902
|
||||
_LOGGER.error(
|
||||
"update: %s: Failed to parse ICS!", self.name, exc_info=True
|
||||
)
|
||||
if self.event is not None:
|
||||
if parser_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,
|
||||
parser_event.summary,
|
||||
parser_event.start,
|
||||
parser_event.end,
|
||||
parser_event.all_day,
|
||||
)
|
||||
(summary, offset) = extract_offset(self.event.summary, OFFSET)
|
||||
self.event.summary = self._summary_prefix + summary
|
||||
(summary, offset) = extract_offset(parser_event.summary, OFFSET)
|
||||
parser_event.summary = self._summary_prefix + summary
|
||||
if not parser_event.summary:
|
||||
parser_event.summary = self._summary_default
|
||||
self.offset = offset
|
||||
# Invoke validation here, since it was skipped when creating the
|
||||
# ParserEvent
|
||||
parser_event.validate()
|
||||
self.event: CalendarEvent = parser_event
|
||||
return True
|
||||
|
||||
_LOGGER.debug("%s: No event found!", self.name)
|
||||
|
@ -1,23 +1,25 @@
|
||||
"""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,
|
||||
)
|
||||
|
||||
import re
|
||||
from logging import Logger
|
||||
from math import floor
|
||||
|
||||
import httpx
|
||||
import httpx_auth
|
||||
from homeassistant.util.dt import now as hanow
|
||||
|
||||
# from urllib.error import ContentTooShortError, HTTPError, URLError
|
||||
|
||||
class CalendarData:
|
||||
|
||||
class DigestWithMultiAuth(httpx.DigestAuth, httpx_auth.SupportMultiAuth):
|
||||
"""Describes a DigestAuth authentication."""
|
||||
|
||||
def __init__(self, username: str, password: str):
|
||||
"""Construct Digest authentication that supports Multi Auth."""
|
||||
httpx.DigestAuth.__init__(self, username, password)
|
||||
|
||||
|
||||
class CalendarData: # pylint: disable=R0902
|
||||
"""CalendarData class.
|
||||
|
||||
The CalendarData class is used to download and cache calendar data from a
|
||||
@ -25,32 +27,33 @@ class CalendarData:
|
||||
instance.
|
||||
"""
|
||||
|
||||
opener_lock = Lock()
|
||||
|
||||
def __init__(
|
||||
self, logger: Logger, name: str, url: str, min_update_time: timedelta
|
||||
self,
|
||||
async_client: httpx.AsyncClient,
|
||||
logger: Logger,
|
||||
conf: dict,
|
||||
):
|
||||
"""Construct CalendarData object.
|
||||
|
||||
:param async_client: An httpx.AsyncClient object for requests
|
||||
:type httpx.AsyncClient
|
||||
: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
|
||||
:param conf: Configuration options
|
||||
:type conf: dict
|
||||
"""
|
||||
self._auth = None
|
||||
self._calendar_data = None
|
||||
self._headers = []
|
||||
self._last_download = None
|
||||
self._min_update_time = min_update_time
|
||||
self._opener = None
|
||||
self._min_update_time = conf["min_update_time"]
|
||||
self.logger = logger
|
||||
self.name = name
|
||||
self.url = url
|
||||
self.name = conf["name"]
|
||||
self.url = conf["url"]
|
||||
self.connection_timeout = None
|
||||
self._httpx = async_client
|
||||
|
||||
def download_calendar(self) -> bool:
|
||||
async def download_calendar(self) -> bool:
|
||||
"""Download the calendar data.
|
||||
|
||||
This only downloads data if self.min_update_time has passed since the
|
||||
@ -59,20 +62,25 @@ class CalendarData:
|
||||
returns: True if data was downloaded, otherwise False.
|
||||
rtype: bool
|
||||
"""
|
||||
now = hanow()
|
||||
self.logger.debug("%s: download_calendar start", self.name)
|
||||
if (
|
||||
self._calendar_data is None
|
||||
or self._last_download is None
|
||||
or (now - self._last_download) > self._min_update_time
|
||||
or (hanow() - self._last_download) > self._min_update_time
|
||||
):
|
||||
self._last_download = now
|
||||
self._calendar_data = None
|
||||
next_url: str = self._make_url()
|
||||
self.logger.debug(
|
||||
"%s: Downloading calendar data from: %s", self.name, self.url
|
||||
"%s: Downloading calendar data from: %s",
|
||||
self.name,
|
||||
next_url,
|
||||
)
|
||||
self._download_data()
|
||||
await self._download_data(next_url)
|
||||
self._last_download = hanow()
|
||||
self.logger.debug("%s: download_calendar done", self.name)
|
||||
return self._calendar_data is not None
|
||||
|
||||
self.logger.debug("%s: download_calendar skipped download", self.name)
|
||||
return False
|
||||
|
||||
def get(self) -> str:
|
||||
@ -92,10 +100,8 @@ class CalendarData:
|
||||
):
|
||||
"""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.
|
||||
The user name and password will be set into an auth object that
|
||||
supports both Basic Auth and Digest Auth for httpx.
|
||||
|
||||
If the user_agent parameter is not "", a User-agent header will be
|
||||
added to the urlopener.
|
||||
@ -110,81 +116,63 @@ class CalendarData:
|
||||
: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
|
||||
)
|
||||
self._auth = httpx_auth.Basic(
|
||||
user_name, password
|
||||
) + DigestWithMultiAuth(user_name, password)
|
||||
|
||||
additional_headers = []
|
||||
if user_agent != "":
|
||||
additional_headers.append(("User-agent", user_agent))
|
||||
self._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
|
||||
self._headers.append(("Accept", accept_header))
|
||||
|
||||
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 set_timeout(self, connection_timeout: float):
|
||||
"""Set the connection timeout.
|
||||
|
||||
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
|
||||
:param connection_timeout: The timeout value in seconds.
|
||||
:type connection_timeout: float
|
||||
"""
|
||||
self.connection_timeout = connection_timeout
|
||||
|
||||
def _download_data(self):
|
||||
def _decode_data(self, data):
|
||||
return data.replace("\0", "")
|
||||
|
||||
async def _download_data(self, url): # noqa: C901
|
||||
"""Download the calendar data."""
|
||||
self.logger.debug("%s: _download_data start", self.name)
|
||||
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:
|
||||
response = await self._httpx.get(
|
||||
url,
|
||||
auth=self._auth,
|
||||
headers=self._headers,
|
||||
follow_redirects=True,
|
||||
timeout=self.connection_timeout,
|
||||
)
|
||||
if response.status_code >= 400:
|
||||
raise httpx.HTTPStatusError(
|
||||
"status error", request=None, response=response
|
||||
)
|
||||
self._calendar_data = self._decode_data(response.text)
|
||||
self.logger.debug("%s: _download_data done", self.name)
|
||||
except httpx.HTTPStatusError as http_status_error:
|
||||
self.logger.error(
|
||||
"%s: Failed to open url(%s): %s",
|
||||
self.name,
|
||||
self.url,
|
||||
http_error.reason,
|
||||
http_status_error.response.status_code,
|
||||
)
|
||||
except ContentTooShortError as content_too_short_error:
|
||||
except httpx.TimeoutException:
|
||||
self.logger.error(
|
||||
"%s: Could not download calendar data: %s",
|
||||
self.name,
|
||||
content_too_short_error.reason,
|
||||
"%s: Timeout opening url: %s", self.name, self.url
|
||||
)
|
||||
except URLError as url_error:
|
||||
except httpx.DecodingError:
|
||||
self.logger.error(
|
||||
"%s: Failed to open url: %s", self.name, url_error.reason
|
||||
"%s: Error decoding data from url: %s", self.name, self.url
|
||||
)
|
||||
except httpx.InvalidURL:
|
||||
self.logger.error("%s: Invalid URL: %s", self.name, self.url)
|
||||
except httpx.HTTPError:
|
||||
self.logger.error(
|
||||
"%s: Error decoding data from url: %s", self.name, self.url
|
||||
)
|
||||
except: # pylint: disable=W0702
|
||||
self.logger.error(
|
||||
@ -192,7 +180,45 @@ class CalendarData:
|
||||
)
|
||||
|
||||
def _make_url(self):
|
||||
"""Replace templates in url and encode."""
|
||||
now = hanow()
|
||||
return self.url.replace("{year}", f"{now.year:04}").replace(
|
||||
"{month}", f"{now.month:02}"
|
||||
year: int = now.year
|
||||
month: int = now.month
|
||||
url = self.url
|
||||
(month, year, url) = self._get_month_year(url, month, year)
|
||||
return url.replace("{year}", f"{year:04}").replace(
|
||||
"{month}", f"{month:02}"
|
||||
)
|
||||
|
||||
def _get_year_as_months(self, url: str, month: int) -> int:
|
||||
year_match = re.search("\\{year([-+])([0-9]+)\\}", url)
|
||||
if year_match:
|
||||
if year_match.group(1) == "-":
|
||||
month = month - (int(year_match.group(2)) * 12)
|
||||
else:
|
||||
month = month + (int(year_match.group(2)) * 12)
|
||||
url = url.replace(year_match.group(0), "{year}")
|
||||
return (month, url)
|
||||
|
||||
def _get_month_year(self, url: str, month: int, year: int) -> int:
|
||||
(month, url) = self._get_year_as_months(url, month)
|
||||
print(f"month: {month}\n")
|
||||
month_match = re.search("\\{month([-+])([0-9]+)\\}", url)
|
||||
if month_match:
|
||||
if month_match.group(1) == "-":
|
||||
month = month - int(month_match.group(2))
|
||||
else:
|
||||
month = month + int(month_match.group(2))
|
||||
if month < 1:
|
||||
year -= floor(abs(month) / 12) + 1
|
||||
month = month % 12
|
||||
if month == 0:
|
||||
month = 12
|
||||
elif month > 12:
|
||||
year += abs(floor(month / 12))
|
||||
month = month % 12
|
||||
if month == 0:
|
||||
month = 12
|
||||
year -= 1
|
||||
url = url.replace(month_match.group(0), "{month}")
|
||||
return (month, year, url)
|
||||
|
330
custom_components/ics_calendar/config_flow.py
Normal file
330
custom_components/ics_calendar/config_flow.py
Normal file
@ -0,0 +1,330 @@
|
||||
"""Config Flow for ICS Calendar."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Dict, Optional, Self
|
||||
from urllib.parse import quote
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import voluptuous as vol
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import (
|
||||
CONF_EXCLUDE,
|
||||
CONF_INCLUDE,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_PREFIX,
|
||||
CONF_URL,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.helpers.selector import selector
|
||||
|
||||
from . import (
|
||||
CONF_ACCEPT_HEADER,
|
||||
CONF_ADV_CONNECT_OPTS,
|
||||
CONF_CONNECTION_TIMEOUT,
|
||||
CONF_DAYS,
|
||||
CONF_DOWNLOAD_INTERVAL,
|
||||
CONF_INCLUDE_ALL_DAY,
|
||||
CONF_OFFSET_HOURS,
|
||||
CONF_PARSER,
|
||||
CONF_REQUIRES_AUTH,
|
||||
CONF_SET_TIMEOUT,
|
||||
CONF_SUMMARY_DEFAULT,
|
||||
CONF_USER_AGENT,
|
||||
)
|
||||
from .const import CONF_SUMMARY_DEFAULT_DEFAULT, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CALENDAR_NAME_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_DAYS, default=1): cv.positive_int,
|
||||
vol.Optional(CONF_INCLUDE_ALL_DAY, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
CALENDAR_OPTS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_EXCLUDE, default=""): cv.string,
|
||||
vol.Optional(CONF_INCLUDE, default=""): cv.string,
|
||||
vol.Optional(CONF_PREFIX, default=""): cv.string,
|
||||
vol.Optional(CONF_DOWNLOAD_INTERVAL, default=15): cv.positive_int,
|
||||
vol.Optional(CONF_OFFSET_HOURS, default=0): int,
|
||||
vol.Optional(CONF_PARSER, default="rie"): selector(
|
||||
{"select": {"options": ["rie", "ics"], "mode": "dropdown"}}
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_SUMMARY_DEFAULT, default=CONF_SUMMARY_DEFAULT_DEFAULT
|
||||
): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
CONNECT_OPTS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
vol.Optional(CONF_REQUIRES_AUTH, default=False): cv.boolean,
|
||||
vol.Optional(CONF_ADV_CONNECT_OPTS, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
AUTH_OPTS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_USERNAME, default=""): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=""): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
ADVANCED_CONNECT_OPTS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ACCEPT_HEADER, default=""): cv.string,
|
||||
vol.Optional(CONF_USER_AGENT, default=""): cv.string,
|
||||
vol.Optional(CONF_SET_TIMEOUT, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
TIMEOUT_OPTS_SCHEMA = vol.Schema(
|
||||
{vol.Optional(CONF_CONNECTION_TIMEOUT, default=None): cv.positive_float}
|
||||
)
|
||||
|
||||
|
||||
def is_array_string(arr_str: str) -> bool:
|
||||
"""Return true if arr_str starts with [ and ends with ]."""
|
||||
return arr_str.startswith("[") and arr_str.endswith("]")
|
||||
|
||||
|
||||
def format_url(url: str) -> str:
|
||||
"""Format a URL using quote() and ensure any templates are not quoted."""
|
||||
is_quoted = bool(re.search("%[0-9A-Fa-f][0-9A-Fa-f]", url))
|
||||
if not is_quoted:
|
||||
year_match = re.search("\\{(year([-+][0-9]+)?)\\}", url)
|
||||
month_match = re.search("\\{(month([-+][0-9]+)?)\\}", url)
|
||||
has_template: bool = year_match or month_match
|
||||
url = quote(url, safe=":/?&=")
|
||||
if has_template:
|
||||
year_template = year_match.group(1)
|
||||
month_template = month_match.group(1)
|
||||
year_template1 = year_template.replace("+", "%2[Bb]")
|
||||
month_template1 = month_template.replace("+", "%2[Bb]")
|
||||
url = re.sub(
|
||||
f"%7[Bb]{year_template1}%7[Dd]",
|
||||
f"{{{year_template}}}",
|
||||
url,
|
||||
)
|
||||
url = re.sub(
|
||||
f"%7[Bb]{month_template1}%7[Dd]",
|
||||
f"{{{month_template}}}",
|
||||
url,
|
||||
)
|
||||
if url.startswith("webcal://"):
|
||||
url = re.sub("^webcal://", "https://", url)
|
||||
|
||||
return url
|
||||
|
||||
|
||||
class ICSCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Config Flow for ICS Calendar."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 0
|
||||
|
||||
data: Optional[Dict[str, Any]]
|
||||
|
||||
def __init__(self):
|
||||
"""Construct ICSCalendarConfigFlow."""
|
||||
self.data = {}
|
||||
|
||||
def is_matching(self, _other_flow: Self) -> bool:
|
||||
"""Match discovery method.
|
||||
|
||||
This method doesn't do anything, because this integration has no
|
||||
discoverable components.
|
||||
"""
|
||||
return False
|
||||
|
||||
async def async_step_reauth(self, user_input=None):
|
||||
"""Re-authenticateon auth error."""
|
||||
# self.reauth_entry = self.hass.config_entries.async_get_entry(
|
||||
# self.context["entry_id"]
|
||||
# )
|
||||
return await self.async_step_reauth_confirm(user_input)
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input=None
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog to inform user that reauthentication is required."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm", data_schema=vol.Schema({})
|
||||
)
|
||||
return await self.async_step_user()
|
||||
|
||||
# Don't allow reconfigure for now!
|
||||
# async def async_step_reconfigure(
|
||||
# self, user_input: dict[str, Any] | None = None
|
||||
# ) -> ConfigFlowResult:
|
||||
# """Reconfigure entry."""
|
||||
# return await self.async_step_user(user_input)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: Optional[Dict[str, Any]] = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Start of Config Flow."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
user_input[CONF_NAME] = user_input[CONF_NAME].strip()
|
||||
if not user_input[CONF_NAME]:
|
||||
errors[CONF_NAME] = "empty_name"
|
||||
else:
|
||||
self._async_abort_entries_match(
|
||||
{CONF_NAME: user_input[CONF_NAME]}
|
||||
)
|
||||
|
||||
if not errors:
|
||||
self.data = user_input
|
||||
return await self.async_step_calendar_opts()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=CALENDAR_NAME_SCHEMA,
|
||||
errors=errors,
|
||||
last_step=False,
|
||||
)
|
||||
|
||||
async def async_step_calendar_opts( # noqa: R701,C901
|
||||
self, user_input: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""Calendar Options step for ConfigFlow."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
user_input[CONF_EXCLUDE] = user_input[CONF_EXCLUDE].strip()
|
||||
user_input[CONF_INCLUDE] = user_input[CONF_INCLUDE].strip()
|
||||
if (
|
||||
user_input[CONF_EXCLUDE]
|
||||
and user_input[CONF_EXCLUDE] == user_input[CONF_INCLUDE]
|
||||
):
|
||||
errors[CONF_EXCLUDE] = "exclude_include_cannot_be_the_same"
|
||||
else:
|
||||
if user_input[CONF_EXCLUDE] and not is_array_string(
|
||||
user_input[CONF_EXCLUDE]
|
||||
):
|
||||
errors[CONF_EXCLUDE] = "exclude_must_be_array"
|
||||
if user_input[CONF_INCLUDE] and not is_array_string(
|
||||
user_input[CONF_INCLUDE]
|
||||
):
|
||||
errors[CONF_INCLUDE] = "include_must_be_array"
|
||||
|
||||
if user_input[CONF_DOWNLOAD_INTERVAL] < 15:
|
||||
_LOGGER.error("download_interval_too_small error")
|
||||
errors[CONF_DOWNLOAD_INTERVAL] = "download_interval_too_small"
|
||||
|
||||
if not user_input[CONF_SUMMARY_DEFAULT]:
|
||||
user_input[CONF_SUMMARY_DEFAULT] = CONF_SUMMARY_DEFAULT_DEFAULT
|
||||
|
||||
if not errors:
|
||||
self.data.update(user_input)
|
||||
return await self.async_step_connect_opts()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="calendar_opts",
|
||||
data_schema=CALENDAR_OPTS_SCHEMA,
|
||||
errors=errors,
|
||||
last_step=False,
|
||||
)
|
||||
|
||||
async def async_step_connect_opts(
|
||||
self, user_input: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""Connect Options step for ConfigFlow."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
user_input[CONF_URL] = user_input[CONF_URL].strip()
|
||||
if not user_input[CONF_URL]:
|
||||
errors[CONF_URL] = "empty_url"
|
||||
|
||||
if not errors:
|
||||
user_input[CONF_URL] = format_url(user_input[CONF_URL])
|
||||
|
||||
self.data.update(user_input)
|
||||
if user_input.get(CONF_REQUIRES_AUTH, False):
|
||||
return await self.async_step_auth_opts()
|
||||
if user_input.get(CONF_ADV_CONNECT_OPTS, False):
|
||||
return await self.async_step_adv_connect_opts()
|
||||
return self.async_create_entry(
|
||||
title=self.data[CONF_NAME],
|
||||
data=self.data,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="connect_opts",
|
||||
data_schema=CONNECT_OPTS_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_auth_opts(
|
||||
self, user_input: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""Auth Options step for ConfigFlow."""
|
||||
if user_input is not None:
|
||||
self.data.update(user_input)
|
||||
if self.data.get(CONF_ADV_CONNECT_OPTS, False):
|
||||
return await self.async_step_adv_connect_opts()
|
||||
return self.async_create_entry(
|
||||
title=self.data[CONF_NAME],
|
||||
data=self.data,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="auth_opts", data_schema=AUTH_OPTS_SCHEMA
|
||||
)
|
||||
|
||||
async def async_step_adv_connect_opts(
|
||||
self, user_input: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""Advanced Connection Options step for ConfigFlow."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
|
||||
if not errors:
|
||||
self.data.update(user_input)
|
||||
if user_input.get(CONF_SET_TIMEOUT, False):
|
||||
return await self.async_step_timeout_opts()
|
||||
return self.async_create_entry(
|
||||
title=self.data[CONF_NAME],
|
||||
data=self.data,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="adv_connect_opts",
|
||||
data_schema=ADVANCED_CONNECT_OPTS_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_timeout_opts(
|
||||
self, user_input: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""Timeout Options step for ConfigFlow."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
|
||||
if not errors:
|
||||
self.data.update(user_input)
|
||||
return self.async_create_entry(
|
||||
title=self.data[CONF_NAME],
|
||||
data=self.data,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="timeout_opts",
|
||||
data_schema=TIMEOUT_OPTS_SCHEMA,
|
||||
errors=errors,
|
||||
last_step=True,
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data):
|
||||
"""Import config from configuration.yaml."""
|
||||
return self.async_create_entry(
|
||||
title=import_data[CONF_NAME],
|
||||
data=import_data,
|
||||
)
|
@ -1,7 +1,24 @@
|
||||
"""Constants for ics_calendar platform."""
|
||||
VERSION = "4.1.0"
|
||||
|
||||
VERSION = "5.1.3"
|
||||
DOMAIN = "ics_calendar"
|
||||
UPGRADE_URL = (
|
||||
"https://github.com/franc6/ics_calendar/blob/releases/"
|
||||
"UpgradeTo4.0AndLater.md"
|
||||
)
|
||||
|
||||
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"
|
||||
CONF_CONNECTION_TIMEOUT = "connection_timeout"
|
||||
CONF_SET_TIMEOUT = "set_connection_timeout"
|
||||
CONF_REQUIRES_AUTH = "requires_auth"
|
||||
CONF_ADV_CONNECT_OPTS = "advanced_connection_options"
|
||||
CONF_SUMMARY_DEFAULT = "summary_default"
|
||||
# It'd be really nifty if this could be a translatable string, but it seems
|
||||
# that's not supported, unless I want to roll my own interpretation of the
|
||||
# translate/*.json files. :(
|
||||
# See also https://github.com/home-assistant/core/issues/125075
|
||||
CONF_SUMMARY_DEFAULT_DEFAULT = "No title"
|
||||
|
@ -1,9 +1,10 @@
|
||||
"""Provide Filter class."""
|
||||
|
||||
import re
|
||||
from ast import literal_eval
|
||||
from typing import List, Optional, Pattern
|
||||
|
||||
from homeassistant.components.calendar import CalendarEvent
|
||||
from .parserevent import ParserEvent
|
||||
|
||||
|
||||
class Filter:
|
||||
@ -113,11 +114,11 @@ class Filter:
|
||||
add_event = self._is_included(summary, description)
|
||||
return add_event
|
||||
|
||||
def filter_event(self, event: CalendarEvent) -> bool:
|
||||
def filter_event(self, event: ParserEvent) -> bool:
|
||||
"""Check if the event should be included or not.
|
||||
|
||||
:param event: The event to examine
|
||||
:type event: CalendarEvent
|
||||
:type event: ParserEvent
|
||||
:return: true if the event should be included, otherwise false
|
||||
:rtype: bool
|
||||
"""
|
||||
|
27
custom_components/ics_calendar/getparser.py
Normal file
27
custom_components/ics_calendar/getparser.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""Provide GetParser class."""
|
||||
|
||||
from .icalendarparser import ICalendarParser
|
||||
from .parsers.parser_ics import ParserICS
|
||||
from .parsers.parser_rie import ParserRIE
|
||||
|
||||
|
||||
class GetParser: # pylint: disable=R0903
|
||||
"""Provide get_parser to return an instance of ICalendarParser.
|
||||
|
||||
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_parser(parser: str, *args) -> ICalendarParser | None:
|
||||
"""Get an instance of the requested parser."""
|
||||
# parser_cls = ICalendarParser.get_class(parser)
|
||||
# if parser_cls is not None:
|
||||
# return parser_cls(*args)
|
||||
if parser == "rie":
|
||||
return ParserRIE(*args)
|
||||
if parser == "ics":
|
||||
return ParserICS(*args)
|
||||
|
||||
return None
|
@ -1,39 +1,14 @@
|
||||
"""Provide ICalendarParser class."""
|
||||
import importlib
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from homeassistant.components.calendar import CalendarEvent
|
||||
|
||||
from .filter import Filter
|
||||
from .parserevent import ParserEvent
|
||||
|
||||
|
||||
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
|
||||
"""Provide interface for various parser classes."""
|
||||
|
||||
def set_content(self, content: str):
|
||||
"""Parse content into a calendar object.
|
||||
@ -57,7 +32,7 @@ class ICalendarParser:
|
||||
end: datetime,
|
||||
include_all_day: bool,
|
||||
offset_hours: int = 0,
|
||||
) -> list[CalendarEvent]:
|
||||
) -> list[ParserEvent]:
|
||||
"""Get a list of events.
|
||||
|
||||
Gets the events from start to end, including or excluding all day
|
||||
@ -71,7 +46,7 @@ class ICalendarParser:
|
||||
: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]
|
||||
:rtype list[ParserEvent]
|
||||
"""
|
||||
|
||||
def get_current_event(
|
||||
@ -80,7 +55,7 @@ class ICalendarParser:
|
||||
now: datetime,
|
||||
days: int,
|
||||
offset_hours: int = 0,
|
||||
) -> Optional[CalendarEvent]:
|
||||
) -> Optional[ParserEvent]:
|
||||
"""Get the current or next event.
|
||||
|
||||
Gets the current event, or the next upcoming event with in the
|
||||
@ -93,5 +68,5 @@ class ICalendarParser:
|
||||
:type days int
|
||||
:param offset_hours the number of hours to offset the event
|
||||
:type offset_hours int
|
||||
:returns a CalendarEvent or None
|
||||
:returns a ParserEvent or None
|
||||
"""
|
||||
|
@ -1,13 +1,13 @@
|
||||
{
|
||||
|
||||
"domain": "ics_calendar",
|
||||
"name": "ics Calendar",
|
||||
"codeowners": ["@franc6"],
|
||||
"config_flow": true,
|
||||
"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"
|
||||
"requirements": ["icalendar~=6.1","python-dateutil>=2.9.0.post0","pytz>=2024.1","recurring_ical_events~=3.5,>=3.5.2","ics==0.7.2","arrow","httpx_auth>=0.22.0,<=0.23.1"],
|
||||
"version": "5.1.3"
|
||||
}
|
||||
|
20
custom_components/ics_calendar/parserevent.py
Normal file
20
custom_components/ics_calendar/parserevent.py
Normal file
@ -0,0 +1,20 @@
|
||||
"""Provide ParserEvent class."""
|
||||
|
||||
import dataclasses
|
||||
|
||||
from homeassistant.components.calendar import CalendarEvent
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ParserEvent(CalendarEvent):
|
||||
"""Class to represent CalendarEvent without validation."""
|
||||
|
||||
def validate(self) -> None:
|
||||
"""Invoke __post_init__ from CalendarEvent."""
|
||||
return super().__post_init__()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Don't do validation steps for this class."""
|
||||
# This is necessary to prevent problems when creating events that don't
|
||||
# have a summary. We'll add a summary after the event is created, not
|
||||
# before, to reduce code repitition.
|
@ -1,14 +1,15 @@
|
||||
"""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 ..parserevent import ParserEvent
|
||||
from ..utility import compare_event_dates
|
||||
|
||||
|
||||
@ -41,7 +42,7 @@ class ParserICS(ICalendarParser):
|
||||
|
||||
def get_event_list(
|
||||
self, start, end, include_all_day: bool, offset_hours: int = 0
|
||||
) -> list[CalendarEvent]:
|
||||
) -> list[ParserEvent]:
|
||||
"""Get a list of events.
|
||||
|
||||
Gets the events from start to end, including or excluding all day
|
||||
@ -55,9 +56,9 @@ class ParserICS(ICalendarParser):
|
||||
: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]
|
||||
:rtype list[ParserEvent]
|
||||
"""
|
||||
event_list: list[CalendarEvent] = []
|
||||
event_list: list[ParserEvent] = []
|
||||
|
||||
if self._calendar is not None:
|
||||
# ics 0.8 takes datetime not Arrow objects
|
||||
@ -75,7 +76,7 @@ class ParserICS(ICalendarParser):
|
||||
# summary = event.summary
|
||||
# elif hasattr(event, "name"):
|
||||
summary = event.name
|
||||
calendar_event: CalendarEvent = CalendarEvent(
|
||||
calendar_event: ParserEvent = ParserEvent(
|
||||
summary=summary,
|
||||
start=ParserICS.get_date(
|
||||
event.begin, event.all_day, offset_hours
|
||||
@ -97,7 +98,7 @@ class ParserICS(ICalendarParser):
|
||||
now: datetime,
|
||||
days: int,
|
||||
offset_hours: int = 0,
|
||||
) -> Optional[CalendarEvent]:
|
||||
) -> Optional[ParserEvent]:
|
||||
"""Get the current or next event.
|
||||
|
||||
Gets the current event, or the next upcoming event with in the
|
||||
@ -110,7 +111,7 @@ class ParserICS(ICalendarParser):
|
||||
:type int
|
||||
:param offset_hours the number of hours to offset the event
|
||||
:type int
|
||||
:returns a CalendarEvent or None
|
||||
:returns a ParserEvent or None
|
||||
"""
|
||||
if self._calendar is None:
|
||||
return None
|
||||
@ -144,7 +145,7 @@ class ParserICS(ICalendarParser):
|
||||
# summary = temp_event.summary
|
||||
# elif hasattr(event, "name"):
|
||||
summary = temp_event.name
|
||||
return CalendarEvent(
|
||||
return ParserEvent(
|
||||
summary=summary,
|
||||
start=ParserICS.get_date(
|
||||
temp_event.begin, temp_event.all_day, offset_hours
|
||||
|
@ -1,13 +1,14 @@
|
||||
"""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 ..parserevent import ParserEvent
|
||||
from ..utility import compare_event_dates
|
||||
|
||||
|
||||
@ -45,7 +46,7 @@ class ParserRIE(ICalendarParser):
|
||||
end: datetime,
|
||||
include_all_day: bool,
|
||||
offset_hours: int = 0,
|
||||
) -> list[CalendarEvent]:
|
||||
) -> list[ParserEvent]:
|
||||
"""Get a list of events.
|
||||
|
||||
Gets the events from start to end, including or excluding all day
|
||||
@ -59,12 +60,12 @@ class ParserRIE(ICalendarParser):
|
||||
: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]
|
||||
:rtype list[ParserEvent]
|
||||
"""
|
||||
event_list: list[CalendarEvent] = []
|
||||
event_list: list[ParserEvent] = []
|
||||
|
||||
if self._calendar is not None:
|
||||
for event in rie.of(self._calendar).between(
|
||||
for event in rie.of(self._calendar, skip_bad_series=True).between(
|
||||
start - timedelta(hours=offset_hours),
|
||||
end - timedelta(hours=offset_hours),
|
||||
):
|
||||
@ -73,7 +74,7 @@ class ParserRIE(ICalendarParser):
|
||||
if all_day and not include_all_day:
|
||||
continue
|
||||
|
||||
calendar_event: CalendarEvent = CalendarEvent(
|
||||
calendar_event: ParserEvent = ParserEvent(
|
||||
summary=event.get("SUMMARY"),
|
||||
start=start,
|
||||
end=end,
|
||||
@ -91,7 +92,7 @@ class ParserRIE(ICalendarParser):
|
||||
now: datetime,
|
||||
days: int,
|
||||
offset_hours: int = 0,
|
||||
) -> Optional[CalendarEvent]:
|
||||
) -> Optional[ParserEvent]:
|
||||
"""Get the current or next event.
|
||||
|
||||
Gets the current event, or the next upcoming event with in the
|
||||
@ -104,17 +105,17 @@ class ParserRIE(ICalendarParser):
|
||||
:type int
|
||||
:param offset_hours the number of hours to offset the event
|
||||
:type offset_hours int
|
||||
:returns a CalendarEvent or None
|
||||
:returns a ParserEvent or None
|
||||
"""
|
||||
if self._calendar is None:
|
||||
return None
|
||||
|
||||
temp_event: CalendarEvent = None
|
||||
temp_event = 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(
|
||||
for event in rie.of(self._calendar, skip_bad_series=True).between(
|
||||
now - timedelta(hours=offset_hours),
|
||||
end - timedelta(hours=offset_hours),
|
||||
):
|
||||
@ -139,7 +140,7 @@ class ParserRIE(ICalendarParser):
|
||||
if temp_event is None:
|
||||
return None
|
||||
|
||||
return CalendarEvent(
|
||||
return ParserEvent(
|
||||
summary=temp_event.get("SUMMARY"),
|
||||
start=temp_start,
|
||||
end=temp_end,
|
||||
|
77
custom_components/ics_calendar/strings.json
Normal file
77
custom_components/ics_calendar/strings.json
Normal file
@ -0,0 +1,77 @@
|
||||
{
|
||||
"issues": {
|
||||
"YAML_Warning": {
|
||||
"title": "YAML configuration is deprecated for ICS Calendar",
|
||||
"description": "YAML configuration of ics_calendar is deprecated and will be removed in ics_calendar v5.0.0. Your configuration items have been imported. Please remove them from your configuration.yaml file."
|
||||
}
|
||||
},
|
||||
"title": "ICS Calendar",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"days": "Days",
|
||||
"include_all_day": "Include all day events?"
|
||||
},
|
||||
"title": "Add Calendar"
|
||||
},
|
||||
"calendar_opts": {
|
||||
"data": {
|
||||
"exclude": "Exclude filter",
|
||||
"include": "Include filter",
|
||||
"prefix": "String to prefix all event summaries",
|
||||
"download_interval": "Download interval (minutes)",
|
||||
"offset_hours": "Number of hours to offset event times",
|
||||
"parser": "Parser (rie or ics)",
|
||||
"summary_default": "Summary if event doesn't have one"
|
||||
},
|
||||
"title": "Calendar Options"
|
||||
},
|
||||
"connect_opts": {
|
||||
"data": {
|
||||
"url": "URL of ICS file",
|
||||
"requires_auth": "Requires authentication?",
|
||||
"advanced_connection_options": "Set advanced connection options?"
|
||||
},
|
||||
"title": "Connection Options"
|
||||
},
|
||||
"auth_opts": {
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"description": "Please note this component supports only HTTP Basic Auth and HTTP Digest Auth. More advanced authentication, like OAuth is not supported at this time.",
|
||||
"title": "Authentication"
|
||||
},
|
||||
"adv_connect_opts": {
|
||||
"data": {
|
||||
"accept_header": "Custom Accept header for broken servers",
|
||||
"user_agent": "Custom User-agent header",
|
||||
"set_connection_timeout": "Change connection timeout?"
|
||||
},
|
||||
"title": "Advanced Connection Options"
|
||||
},
|
||||
"timeout_opts": {
|
||||
"data": {
|
||||
"connection_timeout": "Connection timeout in seconds"
|
||||
},
|
||||
"title": "Connection Timeout Options"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "Authorization failed for calendar. Please re-configured the calendar URL and/or authentication settings.",
|
||||
"title": "Authorization Failure for ICS Calendar"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"empty_name": "The calendar name must not be empty.",
|
||||
"empty_url": "The url must not be empty.",
|
||||
"download_interval_too_small": "The download interval must be at least 15.",
|
||||
"exclude_include_cannot_be_the_same": "The exclude and include strings must not be the same",
|
||||
"exclude_must_be_array": "The exclude option must be an array of strings or regular expressions. See https://github.com/franc6/ics_calendar/blob/releases/README.md#filters for more information.",
|
||||
"include_must_be_array": "The include option must be an array of strings or regular expressions. See https://github.com/franc6/ics_calendar/blob/releases/README.md#filters for more information."
|
||||
},
|
||||
"abort": {
|
||||
}
|
||||
}
|
||||
}
|
76
custom_components/ics_calendar/translations/de.json
Normal file
76
custom_components/ics_calendar/translations/de.json
Normal file
@ -0,0 +1,76 @@
|
||||
{
|
||||
"issues": {
|
||||
"YAML_Warning": {
|
||||
"title": "YAML-Konfiguration für ICS-Kalender ist veraltet",
|
||||
"description": "Die YAML-Konfiguration von ics_calendar ist veraltet und wird in ics_calendar v5.0.0 entfernt. Deine Konfigurationselemente wurden importiert. Bitte entferne sie aus deiner configuration.yaml-Datei."
|
||||
}
|
||||
},
|
||||
"title": "ICS-Kalender",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"days": "Tage",
|
||||
"include_all_day": "Ganztägige Ereignisse einbeziehen?"
|
||||
},
|
||||
"title": "Kalender hinzufügen"
|
||||
},
|
||||
"calendar_opts": {
|
||||
"data": {
|
||||
"exclude": "auszuschließende Ereignisse",
|
||||
"include": "einzuschließende Ereignisse",
|
||||
"prefix": "String, um allen Zusammenfassungen ein Präfix hinzuzufügen",
|
||||
"download_interval": "Download-Intervall (Minuten)",
|
||||
"offset_hours": "Anzahl der Stunden, um Ereigniszeiten zu versetzen",
|
||||
"parser": "Parser (rie oder ics)"
|
||||
},
|
||||
"title": "Kalender-Optionen"
|
||||
},
|
||||
"connect_opts": {
|
||||
"data": {
|
||||
"url": "URL der ICS-Datei",
|
||||
"requires_auth": "Erfordert Authentifizierung?",
|
||||
"advanced_connection_options": "Erweiterte Verbindungsoptionen festlegen?"
|
||||
},
|
||||
"title": "Verbindungsoptionen"
|
||||
},
|
||||
"auth_opts": {
|
||||
"data": {
|
||||
"username": "Benutzername",
|
||||
"password": "Passwort"
|
||||
},
|
||||
"description": "Bitte beachte, dass nur HTTP Basic Auth und HTTP Digest Auth unterstützt wird. Authentifizierungsmethoden wie OAuth werden derzeit nicht unterstützt.",
|
||||
"title": "Authentifizierung"
|
||||
},
|
||||
"adv_connect_opts": {
|
||||
"data": {
|
||||
"accept_header": "Eigener Accept-Header für fehlerhafte Server",
|
||||
"user_agent": "Eigener User-Agent-Header",
|
||||
"set_connection_timeout": "Verbindungstimeout ändern?"
|
||||
},
|
||||
"title": "Erweiterte Verbindungsoptionen"
|
||||
},
|
||||
"timeout_opts": {
|
||||
"data": {
|
||||
"connection_timeout": "Verbindungstimeout in Sekunden"
|
||||
},
|
||||
"title": "Verbindungstimeout-Optionen"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "Die Autorisierung für den Kalender ist fehlgeschlagen. Bitte konfiguriere die Kalender-URL und/oder die Authentifizierungseinstellungen neu.",
|
||||
"title": "Autorisierungsfehler für ICS-Kalender"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"empty_name": "Der Kalendername darf nicht leer sein.",
|
||||
"empty_url": "Die URL darf nicht leer sein.",
|
||||
"download_interval_too_small": "Das Download-Intervall muss mindestens 15 betragen.",
|
||||
"exclude_include_cannot_be_the_same": "Die Ausschluss- und Einschluss-Strings dürfen nicht identisch sein.",
|
||||
"exclude_must_be_array": "Die \"auszuschließenden Ereignisse\" müssen ein Array von Zeichenfolgen oder regulären Ausdrücken sein. Weitere Informationen finden Sie unter https://github.com/franc6/ics_calendar/blob/releases/README.md#filters.",
|
||||
"include_must_be_array": "Die \"einzuschließenden Ereignisse\" müssen ein Array von Zeichenfolgen oder regulären Ausdrücken sein. Weitere Informationen finden Sie unter https://github.com/franc6/ics_calendar/blob/releases/README.md#filters."
|
||||
},
|
||||
"abort": {
|
||||
}
|
||||
}
|
||||
}
|
77
custom_components/ics_calendar/translations/en.json
Normal file
77
custom_components/ics_calendar/translations/en.json
Normal file
@ -0,0 +1,77 @@
|
||||
{
|
||||
"issues": {
|
||||
"YAML_Warning": {
|
||||
"title": "YAML configuration is deprecated for ICS Calendar",
|
||||
"description": "YAML configuration of ics_calendar is deprecated and will be removed in ics_calendar v5.0.0. Your configuration items have been imported. Please remove them from your configuration.yaml file."
|
||||
}
|
||||
},
|
||||
"title": "ICS Calendar",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"days": "Days",
|
||||
"include_all_day": "Include all day events?"
|
||||
},
|
||||
"title": "Add Calendar"
|
||||
},
|
||||
"calendar_opts": {
|
||||
"data": {
|
||||
"exclude": "Exclude filter",
|
||||
"include": "Include filter",
|
||||
"prefix": "String to prefix all event summaries",
|
||||
"download_interval": "Download interval (minutes)",
|
||||
"offset_hours": "Number of hours to offset event times",
|
||||
"parser": "Parser (rie or ics)",
|
||||
"summary_default": "Summary if event doesn't have one"
|
||||
},
|
||||
"title": "Calendar Options"
|
||||
},
|
||||
"connect_opts": {
|
||||
"data": {
|
||||
"url": "URL of ICS file",
|
||||
"requires_auth": "Requires authentication?",
|
||||
"advanced_connection_options": "Set advanced connection options?"
|
||||
},
|
||||
"title": "Connection Options"
|
||||
},
|
||||
"auth_opts": {
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"description": "Please note this component supports only HTTP Basic Auth and HTTP Digest Auth. More advanced authentication, like OAuth is not supported at this time.",
|
||||
"title": "Authentication"
|
||||
},
|
||||
"adv_connect_opts": {
|
||||
"data": {
|
||||
"accept_header": "Custom Accept header for broken servers",
|
||||
"user_agent": "Custom User-agent header",
|
||||
"set_connection_timeout": "Change connection timeout?"
|
||||
},
|
||||
"title": "Advanced Connection Options"
|
||||
},
|
||||
"timeout_opts": {
|
||||
"data": {
|
||||
"connection_timeout": "Connection timeout in seconds"
|
||||
},
|
||||
"title": "Connection Timeout Options"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "Authorization failed for calendar. Please re-configured the calendar URL and/or authentication settings.",
|
||||
"title": "Authorization Failure for ICS Calendar"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"empty_name": "The calendar name must not be empty.",
|
||||
"empty_url": "The url must not be empty.",
|
||||
"download_interval_too_small": "The download interval must be at least 15.",
|
||||
"exclude_include_cannot_be_the_same": "The exclude and include strings must not be the same",
|
||||
"exclude_must_be_array": "The exclude option must be an array of strings or regular expressions. See https://github.com/franc6/ics_calendar/blob/releases/README.md#filters for more information.",
|
||||
"include_must_be_array": "The include option must be an array of strings or regular expressions. See https://github.com/franc6/ics_calendar/blob/releases/README.md#filters for more information."
|
||||
},
|
||||
"abort": {
|
||||
}
|
||||
}
|
||||
}
|
77
custom_components/ics_calendar/translations/es.json
Normal file
77
custom_components/ics_calendar/translations/es.json
Normal file
@ -0,0 +1,77 @@
|
||||
{
|
||||
"issues": {
|
||||
"YAML_Warning": {
|
||||
"title": "La configuración YAML está obsoleta para ICS Calendar",
|
||||
"description": "La configuración YAML de ics_calendar está obsoleta y se eliminará en ics_calendar v5.0.0. Sus elementos de configuración se han importado. Elimínelos de su archivo configuration.yaml."
|
||||
}
|
||||
},
|
||||
"title": "ICS Calendar",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "Nombre",
|
||||
"days": "Días",
|
||||
"include_all_day": "¿Incluir eventos de todo el día?"
|
||||
},
|
||||
"title": "Agregar calendario"
|
||||
},
|
||||
"calendar_opts": {
|
||||
"data": {
|
||||
"exclude": "Excluir filtro",
|
||||
"include": "Incluir filtro",
|
||||
"prefix": "Cadena que precederá a todos los resúmenes de eventos",
|
||||
"download_interval": "Intervalo de descarga (minutos)",
|
||||
"offset_hours": "Número de horas para compensar los tiempos del evento",
|
||||
"parser": "Parser (rie or ics)",
|
||||
"summary_default": "Resumen si el evento no tiene uno"
|
||||
},
|
||||
"title": "Opciones de calendario"
|
||||
},
|
||||
"connect_opts": {
|
||||
"data": {
|
||||
"url": "URL del archivo ICS",
|
||||
"requires_auth": "¿Requiere autentificación?",
|
||||
"advanced_connection_options": "¿Establecer opciones de conexión avanzadas?"
|
||||
},
|
||||
"title": "Opciones de conexión"
|
||||
},
|
||||
"auth_opts": {
|
||||
"data": {
|
||||
"username": "Nombre de usuario",
|
||||
"password": "Contraseña"
|
||||
},
|
||||
"description": "Tenga en cuenta que este componente solo admite la autenticación básica HTTP y la autenticación HTTP Digest. Actualmente, no se admiten autenticaciones más avanzadas, como OAuth.",
|
||||
"title": "Autentificación"
|
||||
},
|
||||
"adv_connect_opts": {
|
||||
"data": {
|
||||
"accept_header": "Encabezado Accept personalizado para servidores rotos",
|
||||
"user_agent": "Encabezado de agente de usuario personalizado",
|
||||
"set_connection_timeout": "¿Cambiar el tiempo de espera de la conexión?"
|
||||
},
|
||||
"title": "Opciones avanzadas de conexión"
|
||||
},
|
||||
"timeout_opts": {
|
||||
"data": {
|
||||
"connection_timeout": "Tiempo de espera de la conexión en segundos"
|
||||
},
|
||||
"title": "Opciones de tiempo de espera de la conexión"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "Error de autorización para el calendario. Vuelva a configurar la URL del calendario y/o los ajustes de autenticación.",
|
||||
"title": "Fallo de autorización para ICS Calendar"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"empty_name": "El nombre del calendario no debe estar vacío.",
|
||||
"empty_url": "La url no debe estar vacía.",
|
||||
"download_interval_too_small": "El intervalo de descarga debe ser de al menos 15.",
|
||||
"exclude_include_cannot_be_the_same": "Las cadenas de exclusión e inclusión no deben ser las mismas",
|
||||
"exclude_must_be_array": "La opción de exclusión debe ser una matriz de cadenas o expresiones regulares. Consulte https://github.com/franc6/ics_calendar/blob/releases/README.md#filters para obtener más información.",
|
||||
"include_must_be_array": "La opción de inclusión debe ser un array de cadenas o expresiones regulares. Consulte https://github.com/franc6/ics_calendar/blob/releases/README.md#filters para obtener más información."
|
||||
},
|
||||
"abort": {
|
||||
}
|
||||
}
|
||||
}
|
76
custom_components/ics_calendar/translations/fr.json
Normal file
76
custom_components/ics_calendar/translations/fr.json
Normal file
@ -0,0 +1,76 @@
|
||||
{
|
||||
"issues": {
|
||||
"YAML_Warning": {
|
||||
"title": "La configuration YAML pour ICS Calendar est obsolète",
|
||||
"description": "La configuration YAML d'ICS Calendar est obsolète et sera supprimée dans la version 5.0.0 d'ics_calendar. Les éléments de votre configuration ont été importés. Veuillez les supprimer de votre fichier configuration.yaml."
|
||||
}
|
||||
},
|
||||
"title": "ICS Calendar",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "Nom",
|
||||
"days": "Jours",
|
||||
"include_all_day": "Inclure les événements à la journée ?"
|
||||
},
|
||||
"title": "Ajouter un calendrier"
|
||||
},
|
||||
"calendar_opts": {
|
||||
"data": {
|
||||
"exclude": "Exclure les événements contenant",
|
||||
"include": "Inclure les événements contenant",
|
||||
"prefix": "Préfixer tous les résumés d'événements avec",
|
||||
"download_interval": "Intervalle de téléchargement (minutes)",
|
||||
"offset_hours": "Décalage à appliquer aux horaires des événements (heures)",
|
||||
"parser": "Parseur (rie ou ics)"
|
||||
},
|
||||
"title": "Options du calendrier"
|
||||
},
|
||||
"connect_opts": {
|
||||
"data": {
|
||||
"url": "URL du fichier ICS",
|
||||
"requires_auth": "Authentification requise ?",
|
||||
"advanced_connection_options": "Définir les options avancées de la connexion ?"
|
||||
},
|
||||
"title": "Options de connexion"
|
||||
},
|
||||
"auth_opts": {
|
||||
"data": {
|
||||
"username": "Utilisateur",
|
||||
"password": "Mot de passe"
|
||||
},
|
||||
"description": "Veuillez noter que cette intégration ne supporte que les modes d'authentification HTTP Basic et HTTP Digest. Les méthodes d'authentification plus avancées, telles que OAuth, ne sont pas supportées actuellement.",
|
||||
"title": "Authentification"
|
||||
},
|
||||
"adv_connect_opts": {
|
||||
"data": {
|
||||
"accept_header": "Entête 'Accept' personnalisée pour les serveurs injoignables",
|
||||
"user_agent": "Entête 'User-agent' personnalisée",
|
||||
"set_connection_timeout": "Modifier le délai maximum autorisé pour la connexion ?"
|
||||
},
|
||||
"title": "Options avancées de connexion"
|
||||
},
|
||||
"timeout_opts": {
|
||||
"data": {
|
||||
"connection_timeout": "Délai maximum autorisé pour la connexion (secondes)"
|
||||
},
|
||||
"title": "Options de délai de connexion"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "L'autorisation a échoué pour le calendrier. Veuillez vérifier l'URL du calendrier et/ou les paramètres d'authentification.",
|
||||
"title": "Échec d'autorisation pour ICS Calendar"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"empty_name": "Le nom du calendrier doit être renseigné.",
|
||||
"empty_url": "L'URL du calendrier doit être renseignée.",
|
||||
"download_interval_too_small": "L'intervalle de téléchargement ne peut pas être inférieur à 15 minutes.",
|
||||
"exclude_include_cannot_be_the_same": "Les valeurs d'exclusion et d'inclusion ne peuvent pas être identiques.",
|
||||
"exclude_must_be_array": "The exclude option must be an array of strings or regular expressions. See https://github.com/franc6/ics_calendar/blob/releases/README.md#filters for more information.",
|
||||
"include_must_be_array": "The include option must be an array of strings or regular expressions. See https://github.com/franc6/ics_calendar/blob/releases/README.md#filters for more information."
|
||||
},
|
||||
"abort": {
|
||||
}
|
||||
}
|
||||
}
|
77
custom_components/ics_calendar/translations/pt-br.json
Normal file
77
custom_components/ics_calendar/translations/pt-br.json
Normal file
@ -0,0 +1,77 @@
|
||||
{
|
||||
"issues": {
|
||||
"YAML_Warning": {
|
||||
"title": "A configuração YAML está obsoleta para o ICS Calendar",
|
||||
"description": "A configuração YAML do ics_calendar está obsoleta e será removida na versão 5.0.0 do ics_calendar. Seus itens de configuração foram importados. Por favor, remova-os do seu arquivo configuration.yaml."
|
||||
}
|
||||
},
|
||||
"title": "ICS Calendar",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "Nome",
|
||||
"days": "Dias",
|
||||
"include_all_day": "Incluir eventos de dia inteiro?"
|
||||
},
|
||||
"title": "Adicionar Calendário"
|
||||
},
|
||||
"calendar_opts": {
|
||||
"data": {
|
||||
"exclude": "Filtro de exclusão",
|
||||
"include": "Filtro de inclusão",
|
||||
"prefix": "Texto para prefixar todos os resumos de eventos",
|
||||
"download_interval": "Intervalo de download (minutos)",
|
||||
"offset_hours": "Número de horas para ajustar os horários dos eventos",
|
||||
"parser": "Parser (rie ou ics)",
|
||||
"summary_default": "Resumo padrão se o evento não tiver um"
|
||||
},
|
||||
"title": "Opções do Calendário"
|
||||
},
|
||||
"connect_opts": {
|
||||
"data": {
|
||||
"url": "URL do arquivo ICS",
|
||||
"requires_auth": "Requer autenticação?",
|
||||
"advanced_connection_options": "Definir opções de conexão avançadas?"
|
||||
},
|
||||
"title": "Opções de Conexão"
|
||||
},
|
||||
"auth_opts": {
|
||||
"data": {
|
||||
"username": "Usuário",
|
||||
"password": "Senha"
|
||||
},
|
||||
"description": "Este componente oferece suporte apenas para HTTP Basic Auth e HTTP Digest Auth. Métodos de autenticação mais avançados, como OAuth, ainda não são suportados.",
|
||||
"title": "Autenticação"
|
||||
},
|
||||
"adv_connect_opts": {
|
||||
"data": {
|
||||
"accept_header": "Cabeçalho Accept personalizado para servidores com problemas",
|
||||
"user_agent": "Cabeçalho User-agent personalizado",
|
||||
"set_connection_timeout": "Alterar tempo limite de conexão?"
|
||||
},
|
||||
"title": "Opções Avançadas de Conexão"
|
||||
},
|
||||
"timeout_opts": {
|
||||
"data": {
|
||||
"connection_timeout": "Tempo limite de conexão em segundos"
|
||||
},
|
||||
"title": "Opções de Tempo Limite de Conexão"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "A autorização falhou para o calendário. Por favor, reconfigure a URL do calendário e/ou as configurações de autenticação.",
|
||||
"title": "Falha de Autorização para o ICS Calendar"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"empty_name": "O nome do calendário não pode estar vazio.",
|
||||
"empty_url": "A URL não pode estar vazia.",
|
||||
"download_interval_too_small": "O intervalo de download deve ser de pelo menos 15.",
|
||||
"exclude_include_cannot_be_the_same": "As strings de exclusão e inclusão não podem ser as mesmas.",
|
||||
"exclude_must_be_array": "A opção de exclusão deve ser um array de strings ou expressões regulares. Veja https://github.com/franc6/ics_calendar/blob/releases/README.md#filters para mais informações.",
|
||||
"include_must_be_array": "A opção de inclusão deve ser um array de strings ou expressões regulares. Veja https://github.com/franc6/ics_calendar/blob/releases/README.md#filters para mais informações."
|
||||
},
|
||||
"abort": {
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
"""Utility methods."""
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
|
||||
@ -9,7 +10,7 @@ def make_datetime(val):
|
||||
return val
|
||||
|
||||
|
||||
def compare_event_dates( # pylint: disable=R0913
|
||||
def compare_event_dates( # pylint: disable=R0913,R0917
|
||||
now, end2, start2, all_day2, end, start, all_day
|
||||
) -> bool:
|
||||
"""Determine if end2 and start2 are newer than end and start."""
|
||||
|
BIN
custom_components/ics_calendar_old.tar.bz2
Normal file
BIN
custom_components/ics_calendar_old.tar.bz2
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user