diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index 48be2fc8ca7..e15a43e360b 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from collections.abc import Mapping import logging from typing import Any @@ -11,6 +12,7 @@ from reolink_aio.exceptions import ( ApiError, CredentialsInvalidError, LoginFirmwareError, + LoginPrivacyModeError, ReolinkError, ) import voluptuous as vol @@ -49,6 +51,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_PROTOCOL = "rtsp" DEFAULT_OPTIONS = {CONF_PROTOCOL: DEFAULT_PROTOCOL} +API_STARTUP_TIME = 5 class ReolinkOptionsFlowHandler(OptionsFlow): @@ -101,6 +104,8 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN): self._host: str | None = None self._username: str = "admin" self._password: str | None = None + self._user_input: dict[str, Any] | None = None + self._disable_privacy: bool = False @staticmethod @callback @@ -198,6 +203,21 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN): self._host = discovery_info.ip return await self.async_step_user() + async def async_step_privacy( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Ask permission to disable privacy mode.""" + if user_input is not None: + self._disable_privacy = True + return await self.async_step_user(self._user_input) + + assert self._user_input is not None + placeholders = {"host": self._user_input[CONF_HOST]} + return self.async_show_form( + step_id="privacy", + description_placeholders=placeholders, + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -219,6 +239,10 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN): host = ReolinkHost(self.hass, user_input, DEFAULT_OPTIONS) try: + if self._disable_privacy: + await host.api.baichuan.set_privacy_mode(enable=False) + # give the camera some time to startup the HTTP API server + await asyncio.sleep(API_STARTUP_TIME) await host.async_init() except UserNotAdmin: errors[CONF_USERNAME] = "not_admin" @@ -227,6 +251,9 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN): except PasswordIncompatible: errors[CONF_PASSWORD] = "password_incompatible" placeholders["special_chars"] = ALLOWED_SPECIAL_CHARS + except LoginPrivacyModeError: + self._user_input = user_input + return await self.async_step_privacy() except CredentialsInvalidError: errors[CONF_PASSWORD] = "invalid_auth" except LoginFirmwareError: diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 1cadc16f818..b72e7bbd00d 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -18,6 +18,10 @@ "username": "Username to login to the Reolink device itself. Not the Reolink cloud account.", "password": "Password to login to the Reolink device itself. Not the Reolink cloud account." } + }, + "privacy": { + "title": "Permission to disable Reolink privacy mode", + "description": "Privacy mode is enabled on Reolink device {host}. By pressing SUBMIT, the privacy mode will be disabled to retrieve the necessary information from the Reolink device. You can abort the setup by pressing X and repeat the setup at a time in which privacy mode can be disabled. After this configuration, you are free to enable the privacy mode again using the privacy mode switch entity. During normal startup the privacy mode will not be disabled. Note however that all entities will be marked unavailable as long as the privacy mode is active." } }, "error": { diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index 5950fc49966..4d474588f38 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -2,7 +2,7 @@ import json from typing import Any -from unittest.mock import ANY, AsyncMock, MagicMock, call +from unittest.mock import ANY, AsyncMock, MagicMock, call, patch from aiohttp import ClientSession from freezegun.api import FrozenDateTimeFactory @@ -11,6 +11,7 @@ from reolink_aio.exceptions import ( ApiError, CredentialsInvalidError, LoginFirmwareError, + LoginPrivacyModeError, ReolinkError, ) @@ -88,6 +89,59 @@ async def test_config_flow_manual_success( assert result["result"].unique_id == TEST_MAC +async def test_config_flow_privacy_success( + hass: HomeAssistant, reolink_connect: MagicMock, mock_setup_entry: MagicMock +) -> None: + """Successful flow when privacy mode is turned on.""" + reolink_connect.baichuan.privacy_mode.return_value = True + reolink_connect.get_host_data.side_effect = LoginPrivacyModeError("Test error") + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + 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"] == "privacy" + assert result["errors"] is None + + assert reolink_connect.baichuan.set_privacy_mode.call_count == 0 + reolink_connect.get_host_data.reset_mock(side_effect=True) + + with patch("homeassistant.components.reolink.config_flow.API_STARTUP_TIME", new=0): + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert reolink_connect.baichuan.set_privacy_mode.call_count == 1 + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_NVR_NAME + assert result["data"] == { + CONF_HOST: TEST_HOST, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_PORT: TEST_PORT, + CONF_USE_HTTPS: TEST_USE_HTTPS, + } + assert result["options"] == { + CONF_PROTOCOL: DEFAULT_PROTOCOL, + } + assert result["result"].unique_id == TEST_MAC + + reolink_connect.baichuan.privacy_mode.return_value = False + + async def test_config_flow_errors( hass: HomeAssistant, reolink_connect: MagicMock, mock_setup_entry: MagicMock ) -> None: