diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 1caf4e79cd5..2077b4a5e29 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -19,7 +19,7 @@ from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN -from .exceptions import ReolinkException, UserNotAdmin +from .exceptions import PasswordIncompatible, ReolinkException, UserNotAdmin from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) @@ -56,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: await host.async_init() - except (UserNotAdmin, CredentialsInvalidError) as err: + except (UserNotAdmin, CredentialsInvalidError, PasswordIncompatible) as err: await host.stop() raise ConfigEntryAuthFailed(err) from err except ( diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index be897a69d7d..6d0381b025f 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -6,6 +6,7 @@ from collections.abc import Mapping import logging from typing import Any +from reolink_aio.api import ALLOWED_SPECIAL_CHARS from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError import voluptuous as vol @@ -29,7 +30,12 @@ from homeassistant.helpers import config_validation as cv, selector from homeassistant.helpers.device_registry import format_mac from .const import CONF_USE_HTTPS, DOMAIN -from .exceptions import ReolinkException, ReolinkWebhookException, UserNotAdmin +from .exceptions import ( + PasswordIncompatible, + ReolinkException, + ReolinkWebhookException, + UserNotAdmin, +) from .host import ReolinkHost from .util import is_connected @@ -206,8 +212,11 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN): errors[CONF_USERNAME] = "not_admin" placeholders["username"] = host.api.username placeholders["userlevel"] = host.api.user_level + except PasswordIncompatible: + errors[CONF_PASSWORD] = "password_incompatible" + placeholders["special_chars"] = ALLOWED_SPECIAL_CHARS except CredentialsInvalidError: - errors[CONF_HOST] = "invalid_auth" + errors[CONF_PASSWORD] = "invalid_auth" except ApiError as err: placeholders["error"] = str(err) errors[CONF_HOST] = "api_error" diff --git a/homeassistant/components/reolink/exceptions.py b/homeassistant/components/reolink/exceptions.py index d166b438f31..2a5a955b9e6 100644 --- a/homeassistant/components/reolink/exceptions.py +++ b/homeassistant/components/reolink/exceptions.py @@ -17,3 +17,7 @@ class ReolinkWebhookException(ReolinkException): class UserNotAdmin(ReolinkException): """Raised when user is not admin.""" + + +class PasswordIncompatible(ReolinkException): + """Raised when the password contains special chars that are incompatible.""" diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index c9989f2c02b..310188b720e 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -11,7 +11,7 @@ from typing import Any, Literal import aiohttp from aiohttp.web import Request -from reolink_aio.api import Host +from reolink_aio.api import ALLOWED_SPECIAL_CHARS, Host from reolink_aio.enums import SubType from reolink_aio.exceptions import NotSupportedError, ReolinkError, SubscriptionError @@ -31,7 +31,12 @@ from homeassistant.helpers.event import async_call_later from homeassistant.helpers.network import NoURLAvailableError, get_url from .const import CONF_USE_HTTPS, DOMAIN -from .exceptions import ReolinkSetupException, ReolinkWebhookException, UserNotAdmin +from .exceptions import ( + PasswordIncompatible, + ReolinkSetupException, + ReolinkWebhookException, + UserNotAdmin, +) DEFAULT_TIMEOUT = 30 FIRST_ONVIF_TIMEOUT = 10 @@ -123,6 +128,13 @@ class ReolinkHost: async def async_init(self) -> None: """Connect to Reolink host.""" + if not self._api.valid_password(): + raise PasswordIncompatible( + "Reolink password contains incompatible special character, " + "please change the password to only contain characters: " + f"a-z, A-Z, 0-9 or {ALLOWED_SPECIAL_CHARS}" + ) + await self._api.get_host_data() if self._api.mac_address is None: diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 3b9aba84634..bcf1c71934d 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -29,6 +29,7 @@ "cannot_connect": "Failed to connect, check the IP address of the camera", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "not_admin": "User needs to be admin, user \"{username}\" has authorisation level \"{userlevel}\"", + "password_incompatible": "Password contains incompatible special character, only these characters are allowed: a-z, A-Z, 0-9 or {special_chars}", "unknown": "[%key:common::config_flow::error::unknown%]", "webhook_exception": "Home Assistant URL is not available, go to Settings > System > Network > Home Assistant URL and correct the URLs, see {more_info}" }, diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index ba845dc1697..6e57a7924e7 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -166,8 +166,23 @@ async def test_config_flow_errors( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - assert result["errors"] == {CONF_HOST: "invalid_auth"} + assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} + reolink_connect.valid_password.return_value = False + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_HOST: TEST_HOST, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {CONF_PASSWORD: "password_incompatible"} + + reolink_connect.valid_password.return_value = True reolink_connect.get_host_data.side_effect = ApiError("Test error") result = await hass.config_entries.flow.async_configure( result["flow_id"],