From a2cd2e0102661dd31e30b5de3c01f5936e333e15 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Sat, 22 Aug 2026 15:20:07 +0000 Subject: [PATCH] rework configflow to take an URL --- homeassistant/components/fritzbox/__init__.py | 53 +++++++- .../components/fritzbox/config_flow.py | 104 ++++++++++----- homeassistant/components/fritzbox/const.py | 3 +- .../components/fritzbox/coordinator.py | 3 +- .../components/fritzbox/strings.json | 19 ++- tests/components/fritzbox/__init__.py | 4 +- tests/components/fritzbox/const.py | 11 +- tests/components/fritzbox/test_config_flow.py | 117 ++++++++++++----- tests/components/fritzbox/test_init.py | 120 +++++++++++++++++- 9 files changed, 350 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 6974b27da3c7..3fe0fbd2cf19 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,14 +1,20 @@ """Support for AVM FRITZ!SmartHome devices.""" from requests.exceptions import ConnectionError as RequestConnectionError, HTTPError +from yarl import URL from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, UnitOfTemperature +from homeassistant.const import ( + CONF_HOST, + CONF_VERIFY_SSL, + EVENT_HOMEASSISTANT_STOP, + UnitOfTemperature, +) from homeassistant.core import Event, HomeAssistant from homeassistant.helpers.device_registry import AnyDeviceEntry from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries -from .const import DOMAIN, LOGGER, PLATFORMS +from .const import DEFAULT_VERIFY_SSL, DOMAIN, LOGGER, PLATFORMS from .coordinator import FritzboxConfigEntry, FritzboxDataUpdateCoordinator @@ -65,6 +71,49 @@ async def async_unload_entry(hass: HomeAssistant, entry: FritzboxConfigEntry) -> return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) +async def async_migrate_entry( + hass: HomeAssistant, config_entry: FritzboxConfigEntry +) -> bool: + """Migrate old config entry to a new format.""" + LOGGER.debug( + "Migrating configuration from version %s.%s", + config_entry.version, + config_entry.minor_version, + ) + if config_entry.version > 1: + # This means the user has downgraded from a future version + return False + + if config_entry.version == 1: + new_data = {**config_entry.data} + if config_entry.minor_version < 2: + LOGGER.debug("Migrate config entry data to URL based configuration") + if "://" not in config_entry.data[CONF_HOST]: + host = URL().build( + scheme="http", + host=config_entry.data[CONF_HOST], + ) + else: + host = config_entry.data[CONF_HOST] + + new_data = { + **config_entry.data, + CONF_HOST: str(host), + CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, + } + + hass.config_entries.async_update_entry( + config_entry, data=new_data, version=1, minor_version=2 + ) + + LOGGER.debug( + "Migration to configuration version %s.%s successful", + config_entry.version, + config_entry.minor_version, + ) + return True + + async def async_remove_config_entry_device( hass: HomeAssistant, entry: FritzboxConfigEntry, device: AnyDeviceEntry ) -> bool: diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 520fe5253dde..f99b409d635e 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -2,35 +2,54 @@ from collections.abc import Mapping import ipaddress -from typing import Any, Self, override -from urllib.parse import urlparse +from typing import TYPE_CHECKING, Any, Self, override from pyfritzhome import Fritzhome, LoginError from requests.exceptions import HTTPError import voluptuous as vol +from yarl import URL from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_URL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.helpers.selector import ( + TextSelector, + TextSelectorConfig, + TextSelectorType, +) from homeassistant.helpers.service_info.ssdp import ( ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_PRESENTATION_URL, ATTR_UPNP_UDN, SsdpServiceInfo, ) -from .const import DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN +from .const import DEFAULT_URL, DEFAULT_USERNAME, DEFAULT_VERIFY_SSL, DOMAIN DATA_SCHEMA_USER = vol.Schema( { - vol.Required(CONF_HOST, default=DEFAULT_HOST): str, + vol.Required(CONF_URL, default=DEFAULT_URL): TextSelector( + config=TextSelectorConfig(type=TextSelectorType.URL) + ), vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): str, - vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_PASSWORD): TextSelector( + config=TextSelectorConfig(type=TextSelectorType.PASSWORD) + ), + vol.Required(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, } ) DATA_SCHEMA_CONFIRM = vol.Schema( { vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): str, - vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_PASSWORD): TextSelector( + config=TextSelectorConfig(type=TextSelectorType.PASSWORD) + ), } ) @@ -44,22 +63,25 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a AVM FRITZ!SmartHome config flow.""" VERSION = 1 + MINOR_VERSION = 2 _name: str def __init__(self) -> None: """Initialize flow.""" - self._host: str | None = None + self._url: str | None = None self._password: str | None = None self._username: str | None = None + self._verify_ssl: bool = DEFAULT_VERIFY_SSL def _get_entry(self, name: str) -> ConfigFlowResult: return self.async_create_entry( title=name, data={ - CONF_HOST: self._host, + CONF_HOST: self._url, CONF_PASSWORD: self._password, CONF_USERNAME: self._username, + CONF_VERIFY_SSL: self._verify_ssl, }, ) @@ -70,7 +92,10 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): def _try_connect(self) -> str: """Try to connect and check auth.""" fritzbox = Fritzhome( - host=self._host, user=self._username, password=self._password + host=self._url, + user=self._username, + password=self._password, + ssl_verify=self._verify_ssl, ) try: fritzbox.login() @@ -92,12 +117,13 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) - - self._host = user_input[CONF_HOST] - self._name = str(user_input[CONF_HOST]) + self._url = user_input[CONF_URL] + self._verify_ssl = user_input[CONF_VERIFY_SSL] self._password = user_input[CONF_PASSWORD] self._username = user_input[CONF_USERNAME] + self._name = str(self._url) + + self._async_abort_entries_match({CONF_HOST: self._url}) result = await self.async_try_connect() @@ -116,32 +142,34 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): self, discovery_info: SsdpServiceInfo ) -> ConfigFlowResult: """Handle a flow initialized by discovery.""" - host = urlparse(discovery_info.ssdp_location).hostname - assert isinstance(host, str) + self._url = discovery_info.upnp[ATTR_UPNP_PRESENTATION_URL] + representation_url = URL(self._url) + + if TYPE_CHECKING: + assert isinstance(representation_url.host, str) if ( - ipaddress.ip_address(host).version == 6 - and ipaddress.ip_address(host).is_link_local + ipaddress.ip_address(representation_url.host).version == 6 + and ipaddress.ip_address(representation_url.host).is_link_local ): return self.async_abort(reason="ignore_ip6_link_local") if uuid := discovery_info.upnp.get(ATTR_UPNP_UDN): uuid = uuid.removeprefix("uuid:") await self.async_set_unique_id(uuid) - self._abort_if_unique_id_configured({CONF_HOST: host}) + self._abort_if_unique_id_configured({CONF_HOST: self._url}) - self._host = host if self.hass.config_entries.flow.async_has_matching_flow(self): return self.async_abort(reason="already_in_progress") # update old and user-configured config entries for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_HOST] == host: + if entry.data[CONF_HOST] == self._url: if uuid and not entry.unique_id: self.hass.config_entries.async_update_entry(entry, unique_id=uuid) return self.async_abort(reason="already_configured") - self._name = str(discovery_info.upnp.get(ATTR_UPNP_FRIENDLY_NAME) or host) + self._name = str(discovery_info.upnp.get(ATTR_UPNP_FRIENDLY_NAME) or self._url) self.context["title_placeholders"] = {"name": self._name} return await self.async_step_confirm() @@ -149,7 +177,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): @override def is_matching(self, other_flow: Self) -> bool: """Return True if other_flow is matching this flow.""" - return other_flow._host == self._host + return other_flow._url == self._url async def async_step_confirm( self, user_input: dict[str, Any] | None = None @@ -179,7 +207,8 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: """Trigger a reauthentication flow.""" - self._host = entry_data[CONF_HOST] + self._url = entry_data[CONF_HOST] + self._verify_ssl = entry_data[CONF_VERIFY_SSL] self._name = str(entry_data[CONF_HOST]) self._username = entry_data[CONF_USERNAME] @@ -200,8 +229,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): if result == RESULT_SUCCESS: return self.async_update_reload_and_abort( self._get_reauth_entry(), - data={ - CONF_HOST: self._host, + data_updates={ CONF_PASSWORD: self._password, CONF_USERNAME: self._username, }, @@ -227,31 +255,37 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Handle a reconfiguration flow initialized by the user.""" errors = {} + reconfigure_entry = self._get_reconfigure_entry() + + self._url = reconfigure_entry.data[CONF_HOST] + self._verify_ssl = reconfigure_entry.data[CONF_VERIFY_SSL] + self._username = reconfigure_entry.data[CONF_USERNAME] + self._password = reconfigure_entry.data[CONF_PASSWORD] if user_input is not None: - self._host = user_input[CONF_HOST] - - reconfigure_entry = self._get_reconfigure_entry() - self._username = reconfigure_entry.data[CONF_USERNAME] - self._password = reconfigure_entry.data[CONF_PASSWORD] + self._url = user_input[CONF_URL] + self._verify_ssl = user_input[CONF_VERIFY_SSL] result = await self.async_try_connect() if result == RESULT_SUCCESS: return self.async_update_reload_and_abort( reconfigure_entry, - data_updates={CONF_HOST: self._host}, + data_updates={ + CONF_HOST: self._url, + CONF_VERIFY_SSL: self._verify_ssl, + }, ) errors["base"] = result - host = self._get_reconfigure_entry().data[CONF_HOST] return self.async_show_form( step_id="reconfigure", data_schema=vol.Schema( { - vol.Required(CONF_HOST, default=host): str, + vol.Required(CONF_URL, default=self._url): str, + vol.Required(CONF_VERIFY_SSL, default=self._verify_ssl): bool, } ), - description_placeholders={"name": host}, + description_placeholders={"name": self._url}, errors=errors, ) diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index 9f1de1e7dea5..0fa84965aa9f 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -13,8 +13,9 @@ ATTR_STATE_WINDOW_OPEN: Final = "window_open" COLOR_MODE: Final = "1" COLOR_TEMP_MODE: Final = "4" -DEFAULT_HOST: Final = "fritz.box" +DEFAULT_URL: Final = "http://fritz.box" DEFAULT_USERNAME: Final = "admin" +DEFAULT_VERIFY_SSL: Final = True DOMAIN: Final = "fritzbox" diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py index 1518ccfaa4df..3aae66e66dac 100644 --- a/homeassistant/components/fritzbox/coordinator.py +++ b/homeassistant/components/fritzbox/coordinator.py @@ -9,7 +9,7 @@ from pyfritzhome.devicetypes import FritzhomeTemplate, FritzhomeTrigger from requests.exceptions import ConnectionError as RequestConnectionError, HTTPError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -62,6 +62,7 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat host=self.config_entry.data[CONF_HOST], user=self.config_entry.data[CONF_USERNAME], password=self.config_entry.data[CONF_PASSWORD], + ssl_verify=self.config_entry.data[CONF_VERIFY_SSL], timeout=20, ) diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index e504dab50ad9..c6e6b9552c89 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -1,7 +1,8 @@ { "common": { - "data_description_host": "The hostname or IP address of your FRITZ!Box router.", "data_description_password": "Password for the user to connect Home Assistant to your FRITZ!Box.", + "data_description_ssl_verify": "Whether to verify the SSL certificate when SSL encryption is used to connect to your FRITZ!Box.", + "data_description_url": "The URL of your FRITZ!Box.", "data_description_username": "Name of the user to connect Home Assistant to your FRITZ!Box." }, "config": { @@ -44,23 +45,27 @@ }, "reconfigure": { "data": { - "host": "[%key:common::config_flow::data::host%]" + "url": "[%key:common::config_flow::data::url%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" }, "data_description": { - "host": "[%key:component::fritzbox::common::data_description_host%]" + "url": "[%key:component::fritzbox::common::data_description_url%]", + "verify_ssl": "[%key:component::fritzbox::common::data_description_ssl_verify%]" }, "description": "Update your configuration information for {name}." }, "user": { "data": { - "host": "[%key:common::config_flow::data::host%]", "password": "[%key:common::config_flow::data::password%]", - "username": "[%key:common::config_flow::data::username%]" + "url": "[%key:common::config_flow::data::url%]", + "username": "[%key:common::config_flow::data::username%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" }, "data_description": { - "host": "[%key:component::fritzbox::common::data_description_host%]", "password": "[%key:component::fritzbox::common::data_description_password%]", - "username": "[%key:component::fritzbox::common::data_description_username%]" + "url": "[%key:component::fritzbox::common::data_description_url%]", + "username": "[%key:component::fritzbox::common::data_description_username%]", + "verify_ssl": "[%key:component::fritzbox::common::data_description_ssl_verify%]" }, "description": "Enter your FRITZ!Box information." } diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index f7bfc44d51ef..81400f538d75 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -27,9 +27,7 @@ async def setup_config_entry( ) -> MockConfigEntry: """Do setup of a MockConfigEntry.""" entry = MockConfigEntry( - domain=DOMAIN, - data=data, - unique_id=unique_id, + domain=DOMAIN, data=data, unique_id=unique_id, version=1, minor_version=2 ) entry.add_to_hass(hass) if device is not None and fritz is not None: diff --git a/tests/components/fritzbox/const.py b/tests/components/fritzbox/const.py index c2895de6b8e1..bdb606bb82ef 100644 --- a/tests/components/fritzbox/const.py +++ b/tests/components/fritzbox/const.py @@ -1,15 +1,22 @@ """Constants for fritzbox tests.""" from homeassistant.components.fritzbox.const import DOMAIN -from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_DEVICES, + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + CONF_VERIFY_SSL, +) MOCK_CONFIG = { DOMAIN: { CONF_DEVICES: [ { - CONF_HOST: "10.0.0.1", + CONF_HOST: "http://10.0.0.1", CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user", + CONF_VERIFY_SSL: False, } ] } diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 1f51b4310daf..224e1cc70c05 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -3,7 +3,6 @@ import dataclasses from unittest import mock from unittest.mock import Mock, patch -from urllib.parse import urlparse from pyfritzhome import LoginError import pytest @@ -11,11 +10,19 @@ from requests.exceptions import HTTPError from homeassistant.components.fritzbox.const import DOMAIN from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER -from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_DEVICES, + CONF_HOST, + CONF_PASSWORD, + CONF_URL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.ssdp import ( ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_PRESENTATION_URL, ATTR_UPNP_UDN, SsdpServiceInfo, ) @@ -24,33 +31,41 @@ from .const import CONF_FAKE_NAME, MOCK_CONFIG from tests.common import MockConfigEntry -MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] +MOCK_USER_DATA = { + CONF_URL: "http://10.0.0.1", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_VERIFY_SSL: False, +} MOCK_SSDP_DATA = { "ip4_valid": SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", - ssdp_location="https://10.0.0.1:12345/test", + ssdp_location="http://10.0.0.1:49000/fboxdesc.xml", upnp={ ATTR_UPNP_FRIENDLY_NAME: CONF_FAKE_NAME, ATTR_UPNP_UDN: "uuid:only-a-test", + ATTR_UPNP_PRESENTATION_URL: "http://10.0.0.1", }, ), "ip6_valid": SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", - ssdp_location="https://[1234::1]:12345/test", + ssdp_location="http://[1234::1]:49000/fboxdesc.xml", upnp={ ATTR_UPNP_FRIENDLY_NAME: CONF_FAKE_NAME, ATTR_UPNP_UDN: "uuid:only-a-test", + ATTR_UPNP_PRESENTATION_URL: "http://[1234::1]", }, ), "ip6_invalid": SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", - ssdp_location="https://[fe80::1%1]:12345/test", + ssdp_location="http://[fe80::1%1]:49000/fboxdesc.xml", upnp={ ATTR_UPNP_FRIENDLY_NAME: CONF_FAKE_NAME, ATTR_UPNP_UDN: "uuid:only-a-test", + ATTR_UPNP_PRESENTATION_URL: "https://[fe80::1%1]", }, ), } @@ -78,10 +93,11 @@ async def test_user(hass: HomeAssistant, fritz: Mock) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "10.0.0.1" - assert result["data"][CONF_HOST] == "10.0.0.1" + assert result["title"] == "http://10.0.0.1" + assert result["data"][CONF_HOST] == "http://10.0.0.1" assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" + assert result["data"][CONF_VERIFY_SSL] is False assert not result["result"].unique_id @@ -123,7 +139,15 @@ async def test_user_not_successful(hass: HomeAssistant, fritz: Mock) -> None: async def test_user_already_configured(hass: HomeAssistant, fritz: Mock) -> None: """Test starting a flow by user when already configured.""" - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "http://10.0.0.1", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_VERIFY_SSL: False, + }, + ) mock_config.add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -141,7 +165,12 @@ async def test_user_already_configured(hass: HomeAssistant, fritz: Mock) -> None async def test_reauth_success(hass: HomeAssistant, fritz: Mock) -> None: """Test starting a reauthentication flow.""" - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], + version=1, + minor_version=2, + ) mock_config.add_to_hass(hass) result = await mock_config.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM @@ -165,7 +194,12 @@ async def test_reauth_auth_failed(hass: HomeAssistant, fritz: Mock) -> None: """Test starting a reauthentication flow with authentication failure.""" fritz().login.side_effect = LoginError("Boom") - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], + version=1, + minor_version=2, + ) mock_config.add_to_hass(hass) result = await mock_config.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM @@ -188,7 +222,12 @@ async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock) -> None: """Test starting a reauthentication flow but no connection found.""" fritz().login.side_effect = OSError("Boom") - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], + version=1, + minor_version=2, + ) mock_config.add_to_hass(hass) result = await mock_config.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM @@ -208,10 +247,15 @@ async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock) -> None: async def test_reconfigure_success(hass: HomeAssistant, fritz: Mock) -> None: """Test starting a reconfigure flow.""" - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], + version=1, + minor_version=2, + ) mock_config.add_to_hass(hass) - assert mock_config.data[CONF_HOST] == "10.0.0.1" + assert mock_config.data[CONF_HOST] == "http://10.0.0.1" assert mock_config.data[CONF_USERNAME] == "fake_user" assert mock_config.data[CONF_PASSWORD] == "fake_pass" @@ -221,26 +265,30 @@ async def test_reconfigure_success(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={ - CONF_HOST: "new_host", - }, + user_input={CONF_URL: "https://new_host:8443", CONF_VERIFY_SSL: True}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" - assert mock_config.data[CONF_HOST] == "new_host" + assert mock_config.data[CONF_HOST] == "https://new_host:8443" assert mock_config.data[CONF_USERNAME] == "fake_user" assert mock_config.data[CONF_PASSWORD] == "fake_pass" + assert mock_config.data[CONF_VERIFY_SSL] is True async def test_reconfigure_failed(hass: HomeAssistant, fritz: Mock) -> None: """Test starting a reconfigure flow with failure.""" fritz().login.side_effect = [OSError("Boom"), None] - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], + version=1, + minor_version=2, + ) mock_config.add_to_hass(hass) - assert mock_config.data[CONF_HOST] == "10.0.0.1" + assert mock_config.data[CONF_HOST] == "http://10.0.0.1" assert mock_config.data[CONF_USERNAME] == "fake_user" assert mock_config.data[CONF_PASSWORD] == "fake_pass" @@ -250,9 +298,7 @@ async def test_reconfigure_failed(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={ - CONF_HOST: "new_host", - }, + user_input={CONF_URL: "https://new_host:8443", CONF_VERIFY_SSL: True}, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reconfigure" @@ -260,16 +306,15 @@ async def test_reconfigure_failed(hass: HomeAssistant, fritz: Mock) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={ - CONF_HOST: "new_host", - }, + user_input={CONF_URL: "https://new_host:8443", CONF_VERIFY_SSL: True}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" - assert mock_config.data[CONF_HOST] == "new_host" + assert mock_config.data[CONF_HOST] == "https://new_host:8443" assert mock_config.data[CONF_USERNAME] == "fake_user" assert mock_config.data[CONF_PASSWORD] == "fake_pass" + assert mock_config.data[CONF_VERIFY_SSL] is True @pytest.mark.parametrize( @@ -303,9 +348,10 @@ async def test_ssdp( ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == CONF_FAKE_NAME - assert result["data"][CONF_HOST] == urlparse(test_data.ssdp_location).hostname + assert result["data"][CONF_HOST] == test_data.upnp[ATTR_UPNP_PRESENTATION_URL] assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" + assert result["data"][CONF_VERIFY_SSL] is True assert result["result"].unique_id == "only-a-test" @@ -325,10 +371,11 @@ async def test_ssdp_no_friendly_name(hass: HomeAssistant, fritz: Mock) -> None: user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"}, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "10.0.0.1" - assert result["data"][CONF_HOST] == "10.0.0.1" + assert result["title"] == "http://10.0.0.1" + assert result["data"][CONF_HOST] == "http://10.0.0.1" assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" + assert result["data"][CONF_VERIFY_SSL] is True assert result["result"].unique_id == "only-a-test" @@ -425,7 +472,15 @@ async def test_ssdp_already_in_progress_host(hass: HomeAssistant, fritz: Mock) - async def test_ssdp_already_configured(hass: HomeAssistant, fritz: Mock) -> None: """Test starting a flow from discovery when already configured.""" - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "http://10.0.0.1", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_VERIFY_SSL: False, + }, + ) mock_config.add_to_hass(hass) assert not mock_config.unique_id diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 25b64f4ac170..3f2eb2ecf5de 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, + CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, STATE_UNAVAILABLE, UnitOfTemperature, @@ -37,12 +38,19 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: entries = hass.config_entries.async_entries() assert entries assert len(entries) == 1 - assert entries[0].data[CONF_HOST] == "10.0.0.1" + assert entries[0].data[CONF_HOST] == "http://10.0.0.1" assert entries[0].data[CONF_PASSWORD] == "fake_pass" assert entries[0].data[CONF_USERNAME] == "fake_user" + assert entries[0].data[CONF_VERIFY_SSL] is False assert fritz.call_count == 1 assert fritz.call_args_list == [ - call(host="10.0.0.1", password="fake_pass", user="fake_user", timeout=20) + call( + host="http://10.0.0.1", + password="fake_pass", + user="fake_user", + ssl_verify=False, + timeout=20, + ) ] @@ -84,6 +92,8 @@ async def test_update_unique_id( domain=DOMAIN, data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], unique_id="any", + version=1, + minor_version=2, ) entry.add_to_hass(hass) @@ -143,6 +153,8 @@ async def test_update_unique_id_no_change( domain=DOMAIN, data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], unique_id="any", + version=1, + minor_version=2, ) entry.add_to_hass(hass) @@ -168,6 +180,8 @@ async def test_unload_remove(hass: HomeAssistant, fritz: Mock) -> None: domain=DOMAIN, data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], unique_id=entity_id, + version=1, + minor_version=2, ) entry.add_to_hass(hass) @@ -207,6 +221,8 @@ async def test_logout_on_stop(hass: HomeAssistant, fritz: Mock) -> None: domain=DOMAIN, data=MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], unique_id=entity_id, + version=1, + minor_version=2, ) entry.add_to_hass(hass) @@ -279,6 +295,8 @@ async def test_raise_config_entry_not_ready_when_offline(hass: HomeAssistant) -> domain=DOMAIN, data={CONF_HOST: "any", **MOCK_CONFIG[DOMAIN][CONF_DEVICES][0]}, unique_id="any", + version=1, + minor_version=2, ) entry.add_to_hass(hass) with patch( @@ -300,6 +318,8 @@ async def test_raise_config_entry_error_when_login_fail(hass: HomeAssistant) -> domain=DOMAIN, data={CONF_HOST: "any", **MOCK_CONFIG[DOMAIN][CONF_DEVICES][0]}, unique_id="any", + version=1, + minor_version=2, ) entry.add_to_hass(hass) with patch( @@ -313,3 +333,99 @@ async def test_raise_config_entry_error_when_login_fail(hass: HomeAssistant) -> entries = hass.config_entries.async_entries() config_entry = entries[0] assert config_entry.state is ConfigEntryState.SETUP_ERROR + + +@pytest.mark.parametrize( + ("old_data", "new_data"), + [ + ( + { + CONF_HOST: "10.0.0.1", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + }, + { + CONF_HOST: "http://10.0.0.1", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_VERIFY_SSL: True, + }, + ), + ( + { + CONF_HOST: "https://10.0.0.1", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + }, + { + CONF_HOST: "https://10.0.0.1", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_VERIFY_SSL: True, + }, + ), + ( + { + CONF_HOST: "1234::1", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + }, + { + CONF_HOST: "http://[1234::1]", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_VERIFY_SSL: True, + }, + ), + ( + { + CONF_HOST: "http://[1234::1]", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + }, + { + CONF_HOST: "http://[1234::1]", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_VERIFY_SSL: True, + }, + ), + ( + { + CONF_HOST: "https://[1234::1]", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + }, + { + CONF_HOST: "https://[1234::1]", + CONF_PASSWORD: "fake_pass", + CONF_USERNAME: "fake_user", + CONF_VERIFY_SSL: True, + }, + ), + ], +) +async def test_migrate_entry( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + fritz: Mock, + old_data: dict, + new_data: dict, +) -> None: + """Test migrate config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=old_data, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.fritzbox.async_setup_entry", + return_value=True, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + assert entry.version == 1 + assert entry.minor_version == 2 + assert entry.data == new_data