mirror of
https://github.com/home-assistant/core.git
synced 2026-08-03 20:24:55 +02:00
ca4fd1d232
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
296 lines
9.6 KiB
Python
296 lines
9.6 KiB
Python
"""Adds config flow for NextDNS."""
|
|
|
|
from collections.abc import Mapping
|
|
import logging
|
|
from typing import Any, override
|
|
|
|
from aiohttp.client_exceptions import ClientConnectorError
|
|
from nextdns import ApiError, InvalidApiKeyError, NextDns
|
|
from tenacity import RetryError
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.config_entries import (
|
|
ConfigEntry,
|
|
ConfigEntryState,
|
|
ConfigFlow,
|
|
ConfigFlowResult,
|
|
ConfigSubentryFlow,
|
|
SubentryFlowResult,
|
|
)
|
|
from homeassistant.const import CONF_API_KEY
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
from homeassistant.helpers.selector import (
|
|
SelectOptionDict,
|
|
SelectSelector,
|
|
SelectSelectorConfig,
|
|
SelectSelectorMode,
|
|
)
|
|
|
|
from .const import CONF_PROFILE_ID, DOMAIN, SUBENTRY_TYPE_PROFILE
|
|
|
|
AUTH_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str})
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
async def async_init_nextdns(hass: HomeAssistant, api_key: str) -> NextDns:
|
|
"""Check if credentials are valid."""
|
|
websession = async_get_clientsession(hass)
|
|
|
|
return await NextDns.create(websession, api_key)
|
|
|
|
|
|
async def async_validate_new_api_key(
|
|
hass: HomeAssistant, user_input: dict[str, Any], profile_ids: list[str]
|
|
) -> dict[str, str]:
|
|
"""Validate the new API key during reconfiguration or reauth."""
|
|
errors: dict[str, str] = {}
|
|
|
|
try:
|
|
nextdns = await async_init_nextdns(hass, user_input[CONF_API_KEY])
|
|
except InvalidApiKeyError:
|
|
errors["base"] = "invalid_api_key"
|
|
except ApiError, ClientConnectorError, RetryError, TimeoutError:
|
|
errors["base"] = "cannot_connect"
|
|
except Exception:
|
|
_LOGGER.exception("Unexpected exception")
|
|
errors["base"] = "unknown"
|
|
else:
|
|
for profile_id in profile_ids:
|
|
if not any(profile.id == profile_id for profile in nextdns.profiles):
|
|
errors["base"] = "profile_not_available"
|
|
break
|
|
|
|
return errors
|
|
|
|
|
|
class NextDnsFlowHandler(ConfigFlow, domain=DOMAIN):
|
|
"""Config flow for NextDNS."""
|
|
|
|
VERSION = 2
|
|
|
|
def __init__(self) -> None:
|
|
"""Initialize the config flow."""
|
|
self.nextdns: NextDns
|
|
self.api_key: str
|
|
|
|
@override
|
|
async def async_step_user(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> ConfigFlowResult:
|
|
"""Handle a flow initialized by the user."""
|
|
errors: dict[str, str] = {}
|
|
|
|
if user_input is not None:
|
|
self.api_key = user_input[CONF_API_KEY]
|
|
|
|
self._async_abort_entries_match({CONF_API_KEY: self.api_key})
|
|
|
|
try:
|
|
self.nextdns = await async_init_nextdns(self.hass, self.api_key)
|
|
except InvalidApiKeyError:
|
|
errors["base"] = "invalid_api_key"
|
|
except ApiError, ClientConnectorError, RetryError, TimeoutError:
|
|
errors["base"] = "cannot_connect"
|
|
except Exception:
|
|
_LOGGER.exception("Unexpected exception")
|
|
errors["base"] = "unknown"
|
|
else:
|
|
return await self.async_step_profiles()
|
|
|
|
return self.async_show_form(
|
|
step_id="user",
|
|
data_schema=AUTH_SCHEMA,
|
|
errors=errors,
|
|
)
|
|
|
|
async def async_step_profiles(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> ConfigFlowResult:
|
|
"""Handle the profiles step."""
|
|
errors: dict[str, str] = {}
|
|
|
|
if user_input is not None:
|
|
profile_id = user_input[CONF_PROFILE_ID]
|
|
|
|
return self.async_create_entry(
|
|
title="NextDNS",
|
|
data={CONF_API_KEY: self.api_key},
|
|
subentries=[
|
|
{
|
|
"subentry_type": SUBENTRY_TYPE_PROFILE,
|
|
"data": {CONF_PROFILE_ID: profile_id},
|
|
"title": self.nextdns.get_profile_name(profile_id),
|
|
"unique_id": profile_id,
|
|
},
|
|
],
|
|
)
|
|
|
|
return self.async_show_form(
|
|
step_id="profiles",
|
|
data_schema=vol.Schema(
|
|
{
|
|
vol.Required(CONF_PROFILE_ID): SelectSelector(
|
|
SelectSelectorConfig(
|
|
options=[
|
|
SelectOptionDict(
|
|
value=profile.id,
|
|
label=profile.name,
|
|
)
|
|
for profile in self.nextdns.profiles
|
|
],
|
|
mode=SelectSelectorMode.LIST,
|
|
)
|
|
)
|
|
}
|
|
),
|
|
errors=errors,
|
|
)
|
|
|
|
async def async_step_reauth(
|
|
self, entry_data: Mapping[str, Any]
|
|
) -> ConfigFlowResult:
|
|
"""Handle configuration by re-auth."""
|
|
return await self.async_step_reauth_confirm()
|
|
|
|
async def async_step_reauth_confirm(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> ConfigFlowResult:
|
|
"""Dialog that informs the user that reauth is required."""
|
|
errors: dict[str, str] = {}
|
|
entry = self._get_reauth_entry()
|
|
|
|
if user_input is not None:
|
|
profile_ids = [
|
|
subentry.data[CONF_PROFILE_ID]
|
|
for subentry in entry.get_subentries_of_type(SUBENTRY_TYPE_PROFILE)
|
|
]
|
|
errors = await async_validate_new_api_key(
|
|
self.hass, user_input, profile_ids
|
|
)
|
|
if errors.get("base") == "profile_not_available":
|
|
return self.async_abort(reason="profile_not_available")
|
|
|
|
if not errors:
|
|
return self.async_update_reload_and_abort(
|
|
entry,
|
|
data_updates=user_input,
|
|
)
|
|
|
|
return self.async_show_form(
|
|
step_id="reauth_confirm",
|
|
data_schema=AUTH_SCHEMA,
|
|
errors=errors,
|
|
)
|
|
|
|
async def async_step_reconfigure(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> ConfigFlowResult:
|
|
"""Handle a reconfiguration flow initialized by the user."""
|
|
errors: dict[str, str] = {}
|
|
entry = self._get_reconfigure_entry()
|
|
|
|
if user_input is not None:
|
|
profile_ids = [
|
|
subentry.data[CONF_PROFILE_ID]
|
|
for subentry in entry.get_subentries_of_type(SUBENTRY_TYPE_PROFILE)
|
|
]
|
|
errors = await async_validate_new_api_key(
|
|
self.hass, user_input, profile_ids
|
|
)
|
|
if errors.get("base") == "profile_not_available":
|
|
return self.async_abort(reason="profile_not_available")
|
|
|
|
if not errors:
|
|
return self.async_update_reload_and_abort(
|
|
entry,
|
|
data_updates=user_input,
|
|
)
|
|
|
|
return self.async_show_form(
|
|
step_id="reconfigure",
|
|
data_schema=AUTH_SCHEMA,
|
|
errors=errors,
|
|
)
|
|
|
|
@classmethod
|
|
@callback
|
|
@override
|
|
def async_get_supported_subentry_types(
|
|
cls, config_entry: ConfigEntry
|
|
) -> dict[str, type[ConfigSubentryFlow]]:
|
|
"""Return subentries supported by this integration."""
|
|
return {SUBENTRY_TYPE_PROFILE: ProfileSubentryFlowHandler}
|
|
|
|
|
|
class ProfileSubentryFlowHandler(ConfigSubentryFlow):
|
|
"""Handle a subentry flow for profile."""
|
|
|
|
def __init__(self) -> None:
|
|
"""Initialize the subentry flow."""
|
|
self.nextdns: NextDns
|
|
|
|
async def async_step_user(
|
|
self,
|
|
user_input: dict[str, Any] | None = None,
|
|
) -> SubentryFlowResult:
|
|
"""Handle the profile step."""
|
|
entry = self._get_entry()
|
|
if entry.state is not ConfigEntryState.LOADED:
|
|
return self.async_abort(reason="entry_not_loaded")
|
|
|
|
errors: dict[str, str] = {}
|
|
|
|
self.nextdns = entry.runtime_data.client
|
|
|
|
if user_input is not None:
|
|
profile_id = user_input[CONF_PROFILE_ID]
|
|
|
|
if any(
|
|
subentry.unique_id == profile_id
|
|
for subentry in entry.get_subentries_of_type(SUBENTRY_TYPE_PROFILE)
|
|
):
|
|
return self.async_abort(reason="already_configured")
|
|
|
|
return self.async_create_entry(
|
|
title=self.nextdns.get_profile_name(profile_id),
|
|
data={CONF_PROFILE_ID: profile_id},
|
|
unique_id=profile_id,
|
|
)
|
|
|
|
# Filter out already configured profiles
|
|
configured_profiles = {
|
|
subentry.data[CONF_PROFILE_ID]
|
|
for subentry in entry.get_subentries_of_type(SUBENTRY_TYPE_PROFILE)
|
|
}
|
|
available_profiles = [
|
|
profile
|
|
for profile in self.nextdns.profiles
|
|
if profile.id not in configured_profiles
|
|
]
|
|
|
|
if not available_profiles:
|
|
return self.async_abort(reason="all_profiles_configured")
|
|
|
|
return self.async_show_form(
|
|
step_id="user",
|
|
data_schema=vol.Schema(
|
|
{
|
|
vol.Required(CONF_PROFILE_ID): SelectSelector(
|
|
SelectSelectorConfig(
|
|
options=[
|
|
SelectOptionDict(
|
|
value=profile.id,
|
|
label=profile.name,
|
|
)
|
|
for profile in available_profiles
|
|
],
|
|
mode=SelectSelectorMode.LIST,
|
|
)
|
|
)
|
|
}
|
|
),
|
|
errors=errors,
|
|
)
|