Skip to content

Commit

Permalink
Configflow (#147)
Browse files Browse the repository at this point in the history
Fixes #

Description of change:

## Formatting, testing, and code coverage
Please note your pull request won't be accepted if you haven't properly
formatted your source code, and ensured the unit tests are appropriate.
Please note if you are not running on Windows, you can either run the
scripts via a bash installation (like git-bash).

- [X] formatstyle.sh reports no errors
- [X] All unit tests pass (test.sh)
- [X] Code coverage has not decreased (test.sh)
  • Loading branch information
franc6 authored Sep 11, 2024
2 parents fecff73 + 3cbc02c commit 53274b3
Show file tree
Hide file tree
Showing 19 changed files with 704 additions and 113 deletions.
19 changes: 17 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
## 4.X.X 2024/06/24
- Made sure the global download lock blocks and ensures min_update_time. (@agroszer)
## 5.0.0 2024/09/10
- Fixed #117 Made sure the global download lock blocks and ensures min_update_time. (@agroszer)
- Add UI configuration support
- Fixed #89
- Fixed #126
- Fixed #140
- Fixed #144

### IMPORTANT
Do **NOT** update to this version from version 3.2.0 or older! Update to versoin 4.0.0 and follow the instructions at [UpgradeTo4.0AndLater.md](https://github.com/franc6/ics_calendar/blob/releases/UpgradeTo4 **first**!

UI configuration is now supported, and configuration via YAML is now deprecated. After installing this update, and after restarting Home Assistant, please remove your existing YAML configuration for ICS calendar. **Your existing configuration has been imported!** Failure to remove the entries doesn't hurt anything, but will cause additional log entries about it.

In a future release, YAML configuration support will be removed entirely, so please be sure to update before that happens, or you will lose your existing configuration.

### HELP WANTED
Since there are now some UI components, it'd be nice to have them in more than just English, and you probably don't want me doing the translations. Please open PRs with translation files if you know how. Thanks!

## 4.2.0 2024/01/15
- Add timeout feature. Thanks to @iamjackg!
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,30 @@ Otherwise, you can install it manually.
Using your HA configuration directory (folder) as a starting point you should now also have this:
```
custom_components/ics_calendar/__init__.py
custom_components/ics_calendar/manifest.json
custom_components/ics_calendar/calendar.py
custom_components/ics_calendar/calendardata.py
custom_components/ics_calendar/config_flow.py
custom_components/ics_calendar/const.py
custom_components/ics_calendar/filter.py
custom_components/ics_calendar/getparser.py
custom_components/ics_calendar/icalendarparser.py
custom_components/ics_calendar/manifest.json
custom_components/ics_calendar/parsers/__init__.py
custom_components/ics_calendar/parsers/parser_ics.py
custom_components/ics_calendar/parsers/parser_rie.py
custom_components/ics_calendar/strings.json
custom_components/ics_calendar/translations/en.json
custom_components/ics_calendar/utility.py
```

## Authentication
This component supports HTTP Basic Auth and HTTP Digest Auth. It does not support more advanced authentication methods.

## Configuration
Configuration is done via UI now. Go to https://my.home-assistant.io/redirect/config/integrations/dashboard and click "Add Integration" to add ICS Calendar. You'll want to do this for each calendar for this integration.

Please note that if you previously used configuration.yaml, you can remove those entries after updating to a version that supports UI configuration.

## Example configuration.yaml
```yaml
ics_calendar:
Expand All @@ -62,7 +75,7 @@ Key | Type | Required | Description
Key | Type | Required | Description
-- | -- | -- | --
`name` | `string` | `True` | A name for the calendar
`url` | `string` | `True` | The URL of the remote calendar
`url` | `string` | `True` | The URL of the calendar (https and file URI schemes are supported)
`accept_header` | `string` | An accept header for servers that are misconfigured, default is not set
`connection_timeout` | `float` | `None` | Sets a timeout in seconds for the connection to download the calendar. Use this if you have frequent connection issues with a calendar
`days` | `positive integer` | `False` | The number of days to look ahead (only affects the attributes of the calendar entity), default is 1
Expand Down
190 changes: 170 additions & 20 deletions custom_components/ics_calendar/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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,
Expand All @@ -14,26 +15,30 @@
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_CALENDARS,
CONF_CONNECTION_TIMEOUT,
CONF_DAYS,
CONF_DOWNLOAD_INTERVAL,
CONF_INCLUDE_ALL_DAY,
CONF_OFFSET_HOURS,
CONF_PARSER,
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"
CONF_CONNECTION_TIMEOUT = "connection_timeout"

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
Expand Down Expand Up @@ -97,26 +102,171 @@
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]:
_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( # noqa: C901,R701 # pylint: disable=R0912,R0915
entry: ConfigEntry,
) -> dict:
"""Initialize missing data."""
data = {}
data[CONF_NAME] = entry.data[CONF_NAME]
data[CONF_URL] = entry.data[CONF_URL]
if CONF_INCLUDE_ALL_DAY in entry.data:
data[CONF_INCLUDE_ALL_DAY] = entry.data[CONF_INCLUDE_ALL_DAY]
else:
data[CONF_INCLUDE_ALL_DAY] = False
if CONF_USERNAME in entry.data:
data[CONF_USERNAME] = entry.data[CONF_USERNAME]
else:
data[CONF_USERNAME] = ""
if CONF_PASSWORD in entry.data:
data[CONF_PASSWORD] = entry.data[CONF_PASSWORD]
else:
data[CONF_PASSWORD] = ""
if CONF_PARSER in entry.data:
data[CONF_PARSER] = entry.data[CONF_PARSER]
else:
data[CONF_PARSER] = "rie"
if CONF_PREFIX in entry.data:
data[CONF_PREFIX] = entry.data[CONF_PREFIX]
else:
data[CONF_PREFIX] = ""
if CONF_DAYS in entry.data:
data[CONF_DAYS] = entry.data[CONF_DAYS]
else:
data[CONF_DAYS] = 1
if CONF_DOWNLOAD_INTERVAL in entry.data:
data[CONF_DOWNLOAD_INTERVAL] = entry.data[CONF_DOWNLOAD_INTERVAL]
else:
data[CONF_DOWNLOAD_INTERVAL] = 15
if CONF_USER_AGENT in entry.data:
data[CONF_USER_AGENT] = entry.data[CONF_USER_AGENT]
else:
data[CONF_USER_AGENT] = ""
if CONF_EXCLUDE in entry.data:
data[CONF_EXCLUDE] = entry.data[CONF_EXCLUDE]
else:
data[CONF_EXCLUDE] = ""
if CONF_INCLUDE in entry.data:
data[CONF_INCLUDE] = entry.data[CONF_INCLUDE]
else:
data[CONF_INCLUDE] = ""
if CONF_OFFSET_HOURS in entry.data:
data[CONF_OFFSET_HOURS] = entry.data[CONF_OFFSET_HOURS]
else:
data[CONF_OFFSET_HOURS] = 0
if CONF_ACCEPT_HEADER in entry.data:
data[CONF_ACCEPT_HEADER] = entry.data[CONF_ACCEPT_HEADER]
else:
data[CONF_ACCEPT_HEADER] = ""
if CONF_CONNECTION_TIMEOUT in entry.data:
data[CONF_CONNECTION_TIMEOUT] = entry.data[CONF_CONNECTION_TIMEOUT]
else:
data[CONF_CONNECTION_TIMEOUT] = None

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
37 changes: 31 additions & 6 deletions custom_components/ics_calendar/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
extract_offset,
is_offset_reached,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_EXCLUDE,
CONF_INCLUDE,
Expand All @@ -29,7 +30,8 @@
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,
Expand All @@ -39,10 +41,10 @@
CONF_OFFSET_HOURS,
CONF_PARSER,
CONF_USER_AGENT,
DOMAIN,
)
from .calendardata import CalendarData
from .filter import Filter
from .icalendarparser import ICalendarParser
from .getparser import GetParser

_LOGGER = logging.getLogger(__name__)

Expand All @@ -53,6 +55,22 @@
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."""
data = hass.data[DOMAIN][config_entry.entry_id]
device_id = f"{data[CONF_NAME]}"
entity = ICSCalendarEntity(
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,
Expand All @@ -72,8 +90,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 = []
Expand Down Expand Up @@ -105,7 +128,7 @@ def setup_platform(
class ICSCalendarEntity(CalendarEntity):
"""A CalendarEntity for an ICS Calendar."""

def __init__(self, entity_id: str, device_data):
def __init__(self, entity_id: str, device_data, unique_id: str = None):
"""Construct ICSCalendarEntity.
:param entity_id: Entity id for the calendar
Expand All @@ -120,6 +143,7 @@ def __init__(self, entity_id: str, device_data):
)
self.data = ICSCalendarData(device_data)
self.entity_id = entity_id
self.unique_id = unique_id
self._event = None
self._name = device_data[CONF_NAME]
self._last_call = None
Expand Down Expand Up @@ -224,7 +248,7 @@ def __init__(self, device_data):
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 = GetParser.get_parser(device_data[CONF_PARSER])
self.parser.set_filter(
Filter(device_data[CONF_EXCLUDE], device_data[CONF_INCLUDE])
)
Expand Down Expand Up @@ -281,6 +305,7 @@ async def async_get_events(
event_list = []

for event in event_list:
print("Adding prefix to summary 1")
event.summary = self._summary_prefix + event.summary

return event_list
Expand Down
Loading

0 comments on commit 53274b3

Please sign in to comment.