-
-
Notifications
You must be signed in to change notification settings - Fork 28.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add config flow to CalDAV #103215
Merged
allenporter
merged 9 commits into
home-assistant:dev
from
allenporter:caldav-config-flow
Nov 3, 2023
Merged
Add config flow to CalDAV #103215
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
b61ee76
Initial caldav config flow with broken calendar platform
allenporter 59701b6
Set up calendar entities
allenporter daa294e
Remove separate caldav entity
allenporter 1a526d2
Update tests after merge
allenporter a6ff273
Readbility improvements
allenporter c7eeaa7
Address lint issues
allenporter 63d2257
Apply suggestions from code review
allenporter 3f90fe4
Add checking for duplicate configuration entries
allenporter 3420cb2
Use verify SSL as input into caldav and simplify test setup
allenporter File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,61 @@ | ||
"""The caldav component.""" | ||
|
||
import logging | ||
|
||
import caldav | ||
from caldav.lib.error import AuthorizationError, DAVError | ||
import requests | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import ( | ||
CONF_PASSWORD, | ||
CONF_URL, | ||
CONF_USERNAME, | ||
CONF_VERIFY_SSL, | ||
Platform, | ||
) | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady | ||
|
||
from .const import DOMAIN | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
PLATFORMS: list[Platform] = [Platform.CALENDAR] | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Set up CalDAV from a config entry.""" | ||
hass.data.setdefault(DOMAIN, {}) | ||
|
||
client = caldav.DAVClient( | ||
entry.data[CONF_URL], | ||
username=entry.data[CONF_USERNAME], | ||
password=entry.data[CONF_PASSWORD], | ||
ssl_verify_cert=entry.data[CONF_VERIFY_SSL], | ||
) | ||
try: | ||
await hass.async_add_executor_job(client.principal) | ||
except AuthorizationError as err: | ||
if err.reason == "Unauthorized": | ||
raise ConfigEntryAuthFailed("Credentials error from CalDAV server") from err | ||
# AuthorizationError can be raised if the url is incorrect or | ||
# on some other unexpected server response. | ||
_LOGGER.warning("Unexpected CalDAV server response: %s", err) | ||
return False | ||
except requests.ConnectionError as err: | ||
raise ConfigEntryNotReady("Connection error from CalDAV server") from err | ||
except DAVError as err: | ||
raise ConfigEntryNotReady("CalDAV client error") from err | ||
|
||
hass.data[DOMAIN][entry.entry_id] = client | ||
|
||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
|
||
return True | ||
|
||
|
||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Unload a config entry.""" | ||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
"""Configuration flow for CalDav.""" | ||
|
||
from collections.abc import Mapping | ||
import logging | ||
from typing import Any | ||
|
||
import caldav | ||
from caldav.lib.error import AuthorizationError, DAVError | ||
import requests | ||
import voluptuous as vol | ||
|
||
from homeassistant import config_entries | ||
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL | ||
from homeassistant.data_entry_flow import FlowResult | ||
from homeassistant.helpers import config_validation as cv | ||
|
||
from .const import DOMAIN | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
STEP_USER_DATA_SCHEMA = vol.Schema( | ||
{ | ||
vol.Required(CONF_URL): str, | ||
vol.Required(CONF_USERNAME): cv.string, | ||
vol.Optional(CONF_PASSWORD, default=""): cv.string, | ||
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, | ||
MartinHjelmare marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
) | ||
|
||
|
||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): | ||
"""Handle a config flow for caldav.""" | ||
|
||
VERSION = 1 | ||
_reauth_entry: config_entries.ConfigEntry | None = None | ||
|
||
async def async_step_user( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> FlowResult: | ||
"""Handle the initial step.""" | ||
errors: dict[str, str] = {} | ||
if user_input is not None: | ||
self._async_abort_entries_match( | ||
{ | ||
CONF_URL: user_input[CONF_URL], | ||
CONF_USERNAME: user_input[CONF_USERNAME], | ||
} | ||
) | ||
if error := await self._test_connection(user_input): | ||
allenporter marked this conversation as resolved.
Show resolved
Hide resolved
|
||
errors["base"] = error | ||
else: | ||
return self.async_create_entry( | ||
title=user_input[CONF_USERNAME], data=user_input | ||
) | ||
|
||
return self.async_show_form( | ||
step_id="user", | ||
data_schema=STEP_USER_DATA_SCHEMA, | ||
errors=errors, | ||
) | ||
|
||
async def _test_connection(self, user_input: dict[str, Any]) -> str | None: | ||
"""Test the connection to the CalDAV server and return an error if any.""" | ||
client = caldav.DAVClient( | ||
user_input[CONF_URL], | ||
username=user_input[CONF_USERNAME], | ||
password=user_input[CONF_PASSWORD], | ||
ssl_verify_cert=user_input[CONF_VERIFY_SSL], | ||
) | ||
try: | ||
await self.hass.async_add_executor_job(client.principal) | ||
except AuthorizationError as err: | ||
_LOGGER.warning("Authorization Error connecting to CalDAV server: %s", err) | ||
if err.reason == "Unauthorized": | ||
return "invalid_auth" | ||
# AuthorizationError can be raised if the url is incorrect or | ||
# on some other unexpected server response. | ||
return "cannot_connect" | ||
except requests.ConnectionError as err: | ||
_LOGGER.warning("Connection Error connecting to CalDAV server: %s", err) | ||
return "cannot_connect" | ||
except DAVError as err: | ||
_LOGGER.warning("CalDAV client error: %s", err) | ||
return "cannot_connect" | ||
except Exception: # pylint: disable=broad-except | ||
_LOGGER.exception("Unexpected exception") | ||
return "unknown" | ||
return None | ||
|
||
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: | ||
"""Perform reauth upon an API authentication error.""" | ||
self._reauth_entry = self.hass.config_entries.async_get_entry( | ||
self.context["entry_id"] | ||
) | ||
return await self.async_step_reauth_confirm() | ||
|
||
async def async_step_reauth_confirm( | ||
self, user_input: dict[str, str] | None = None | ||
) -> FlowResult: | ||
"""Confirm reauth dialog.""" | ||
errors = {} | ||
assert self._reauth_entry | ||
if user_input is not None: | ||
user_input = {**self._reauth_entry.data, **user_input} | ||
|
||
if error := await self._test_connection(user_input): | ||
errors["base"] = error | ||
else: | ||
self.hass.config_entries.async_update_entry( | ||
self._reauth_entry, data=user_input | ||
) | ||
await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) | ||
return self.async_abort(reason="reauth_successful") | ||
|
||
return self.async_show_form( | ||
description_placeholders={ | ||
CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME], | ||
}, | ||
step_id="reauth_confirm", | ||
data_schema=vol.Schema( | ||
{ | ||
vol.Required(CONF_PASSWORD): str, | ||
} | ||
), | ||
errors=errors, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
"""Constands for CalDAV.""" | ||
|
||
from typing import Final | ||
|
||
DOMAIN: Final = "caldav" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
{ | ||
"config": { | ||
"step": { | ||
"user": { | ||
"data": { | ||
"url": "[%key:common::config_flow::data::url%]", | ||
"username": "[%key:common::config_flow::data::username%]", | ||
"password": "[%key:common::config_flow::data::password%]", | ||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" | ||
}, | ||
"description": "Please enter your CalDAV server credentials" | ||
}, | ||
"reauth_confirm": { | ||
"description": "The password for {username} is invalid.", | ||
"title": "[%key:common::config_flow::title::reauth%]", | ||
"data": { | ||
"password": "[%key:common::config_flow::data::password%]" | ||
} | ||
} | ||
}, | ||
"create_entry": { | ||
"default": "[%key:common::config_flow::create_entry::authenticated%]" | ||
}, | ||
"error": { | ||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", | ||
"unknown": "[%key:common::config_flow::error::unknown%]" | ||
}, | ||
"abort": { | ||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", | ||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -78,6 +78,7 @@ | |
"bsblan", | ||
"bthome", | ||
"buienradar", | ||
"caldav", | ||
"canary", | ||
"cast", | ||
"cert_expiry", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we going to deprecate the
is_offset_reached
for the legacy entities or deprecate the legacy entities? If so, I think this is ok, but otherwise our standard nowadays is to set the state attribute toNone
when we don't know it for the entity.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I would generally like to make it go away slowly, given its weird and hacky and triggers should be the new way to handle this. My thought was to no longer support it from the UI but preserve it for existing yaml users. We could technically support it here since its just pulled from the titles of the calendars.
I was thinking this needed to be announced when making yaml go away, but perhaps i need to start shifting the docs since I kind of punted on what to do about existing yaml for now, assuming it will be possible to deprecate in the future. (I think we may need a better solution for search still like a calendar helper to fully make it go away)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably need to at least say in the docs if it's not supported when setting up the integration from the UI.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, i've added a documentation PR (which was missing in the first place) and have addressed this.