diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index 5cf91325d70..4c54dc721e1 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -10,7 +10,7 @@ from devolo_plc_api.exceptions.device import DeviceNotFound, DeviceUnavailable from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_IP_ADDRESS, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.httpx_client import get_async_client @@ -40,6 +40,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ip=entry.data[CONF_IP_ADDRESS], zeroconf_instance=zeroconf_instance ) await device.async_connect(session_instance=async_client) + device.password = entry.data.get( + CONF_PASSWORD, "" # This key was added in HA Core 2022.6 + ) except DeviceNotFound as err: raise ConfigEntryNotReady( f"Unable to connect to {entry.data[CONF_IP_ADDRESS]}" diff --git a/homeassistant/components/devolo_home_network/config_flow.py b/homeassistant/components/devolo_home_network/config_flow.py index c96126f43e2..0acdc9cfa64 100644 --- a/homeassistant/components/devolo_home_network/config_flow.py +++ b/homeassistant/components/devolo_home_network/config_flow.py @@ -1,6 +1,7 @@ """Config flow for devolo Home Network integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -10,7 +11,7 @@ import voluptuous as vol from homeassistant import config_entries, core from homeassistant.components import zeroconf -from homeassistant.const import CONF_HOST, CONF_IP_ADDRESS, CONF_NAME +from homeassistant.const import CONF_HOST, CONF_IP_ADDRESS, CONF_NAME, CONF_PASSWORD from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.httpx_client import get_async_client @@ -19,6 +20,7 @@ from .const import DOMAIN, PRODUCT, SERIAL_NUMBER, TITLE _LOGGER = logging.getLogger(__name__) STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_IP_ADDRESS): str}) +STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Optional(CONF_PASSWORD): str}) async def validate_input( @@ -68,6 +70,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): else: await self.async_set_unique_id(info[SERIAL_NUMBER], raise_on_progress=False) self._abort_if_unique_id_configured() + user_input[CONF_PASSWORD] = "" return self.async_create_entry(title=info[TITLE], data=user_input) return self.async_show_form( @@ -100,9 +103,46 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: data = { CONF_IP_ADDRESS: self.context[CONF_HOST], + CONF_PASSWORD: "", } return self.async_create_entry(title=title, data=data) return self.async_show_form( step_id="zeroconf_confirm", description_placeholders={"host_name": title}, ) + + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + """Handle reauthentication.""" + self.context[CONF_HOST] = data[CONF_IP_ADDRESS] + self.context["title_placeholders"][PRODUCT] = self.hass.data[DOMAIN][ + self.context["entry_id"] + ]["device"].product + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by reauthentication.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=STEP_REAUTH_DATA_SCHEMA, + ) + + reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + assert reauth_entry is not None + + data = { + CONF_IP_ADDRESS: self.context[CONF_HOST], + CONF_PASSWORD: user_input[CONF_PASSWORD], + } + self.hass.config_entries.async_update_entry( + reauth_entry, + data=data, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(reauth_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") diff --git a/homeassistant/components/devolo_home_network/strings.json b/homeassistant/components/devolo_home_network/strings.json index 63be57d9485..6c320710a1b 100644 --- a/homeassistant/components/devolo_home_network/strings.json +++ b/homeassistant/components/devolo_home_network/strings.json @@ -8,6 +8,11 @@ "ip_address": "[%key:common::config_flow::data::ip%]" } }, + "reauth_confirm": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + }, "zeroconf_confirm": { "description": "Do you want to add the devolo home network device with the hostname `{host_name}` to Home Assistant?", "title": "Discovered devolo home network device" @@ -19,7 +24,8 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "home_control": "The devolo Home Control Central Unit does not work with this integration." + "home_control": "The devolo Home Control Central Unit does not work with this integration.", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } diff --git a/homeassistant/components/devolo_home_network/translations/en.json b/homeassistant/components/devolo_home_network/translations/en.json index 39c0b6d331f..e98984738b0 100644 --- a/homeassistant/components/devolo_home_network/translations/en.json +++ b/homeassistant/components/devolo_home_network/translations/en.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Device is already configured", - "home_control": "The devolo Home Control Central Unit does not work with this integration." + "home_control": "The devolo Home Control Central Unit does not work with this integration.", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", @@ -10,6 +11,11 @@ }, "flow_title": "{product} ({name})", "step": { + "reauth_confirm": { + "data": { + "password": "Password" + } + }, "user": { "data": { "ip_address": "IP Address" diff --git a/tests/components/devolo_home_network/__init__.py b/tests/components/devolo_home_network/__init__.py index c8561f485ca..f42abef20ec 100644 --- a/tests/components/devolo_home_network/__init__.py +++ b/tests/components/devolo_home_network/__init__.py @@ -1,6 +1,6 @@ """Tests for the devolo Home Network integration.""" from homeassistant.components.devolo_home_network.const import DOMAIN -from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import HomeAssistant from .const import IP @@ -12,6 +12,7 @@ def configure_integration(hass: HomeAssistant) -> MockConfigEntry: """Configure the integration.""" config = { CONF_IP_ADDRESS: IP, + CONF_PASSWORD: "", } entry = MockConfigEntry(domain=DOMAIN, data=config) entry.add_to_hass(hass) diff --git a/tests/components/devolo_home_network/test_config_flow.py b/tests/components/devolo_home_network/test_config_flow.py index 9f05d0af2fb..0d35630407e 100644 --- a/tests/components/devolo_home_network/test_config_flow.py +++ b/tests/components/devolo_home_network/test_config_flow.py @@ -7,17 +7,18 @@ from unittest.mock import patch from devolo_plc_api.exceptions.device import DeviceNotFound import pytest -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.devolo_home_network import config_flow from homeassistant.components.devolo_home_network.const import ( DOMAIN, SERIAL_NUMBER, TITLE, ) -from homeassistant.const import CONF_BASE, CONF_IP_ADDRESS, CONF_NAME +from homeassistant.const import CONF_BASE, CONF_IP_ADDRESS, CONF_NAME, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from . import configure_integration from .const import DISCOVERY_INFO, DISCOVERY_INFO_WRONG_DEVICE, IP from .mock import MockDevice @@ -47,6 +48,7 @@ async def test_form(hass: HomeAssistant, info: dict[str, Any]): assert result2["title"] == info["title"] assert result2["data"] == { CONF_IP_ADDRESS: IP, + CONF_PASSWORD: "", } assert len(mock_setup_entry.mock_calls) == 1 @@ -112,6 +114,7 @@ async def test_zeroconf(hass: HomeAssistant): assert result2["title"] == "test" assert result2["data"] == { CONF_IP_ADDRESS: IP, + CONF_PASSWORD: "", } @@ -168,6 +171,47 @@ async def test_abort_if_configued(hass: HomeAssistant): assert result3["reason"] == "already_configured" +@pytest.mark.usefixtures("mock_device") +@pytest.mark.usefixtures("mock_zeroconf") +async def test_form_reauth(hass: HomeAssistant): + """Test that the reauth confirmation form is served.""" + entry = configure_integration(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "title_placeholders": { + CONF_NAME: DISCOVERY_INFO.hostname.split(".")[0], + }, + }, + data=entry.data, + ) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + with patch( + "homeassistant.components.devolo_home_network.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "test-password-new"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + assert len(mock_setup_entry.mock_calls) == 1 + + await hass.config_entries.async_unload(entry.entry_id) + + +@pytest.mark.usefixtures("mock_device") +@pytest.mark.usefixtures("mock_zeroconf") async def test_validate_input(hass: HomeAssistant): """Test input validation.""" with patch( diff --git a/tests/components/devolo_home_network/test_init.py b/tests/components/devolo_home_network/test_init.py index 5d5693c44e3..524590d7ead 100644 --- a/tests/components/devolo_home_network/test_init.py +++ b/tests/components/devolo_home_network/test_init.py @@ -4,13 +4,17 @@ from unittest.mock import patch from devolo_plc_api.exceptions.device import DeviceNotFound import pytest +from homeassistant.components.devolo_home_network.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_IP_ADDRESS, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from . import configure_integration +from .const import IP from .mock import MockDevice +from tests.common import MockConfigEntry + @pytest.mark.usefixtures("mock_device") async def test_setup_entry(hass: HomeAssistant): @@ -24,6 +28,22 @@ async def test_setup_entry(hass: HomeAssistant): assert entry.state is ConfigEntryState.LOADED +@pytest.mark.usefixtures("mock_device") +async def test_setup_without_password(hass: HomeAssistant): + """Test setup entry without a device password set like used before HA Core 2022.06.""" + config = { + CONF_IP_ADDRESS: IP, + } + entry = MockConfigEntry(domain=DOMAIN, data=config) + entry.add_to_hass(hass) + with patch( + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + return_value=True, + ), patch("homeassistant.core.EventBus.async_listen_once"): + assert await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.LOADED + + async def test_setup_device_not_found(hass: HomeAssistant): """Test setup entry.""" entry = configure_integration(hass)