Compare commits

...

9 Commits

Author SHA1 Message Date
mib1185
0b4d367046 add missing assert 2026-02-15 22:11:11 +00:00
mib1185
fdae65a343 fix typo 2026-02-15 22:09:41 +00:00
mib1185
a7174fe325 remove port attribut as the lib takes a full URL as host attribut 2026-02-15 20:47:56 +00:00
mib1185
106558898a ensure ipv6 config is migrated properly 2026-02-15 20:05:49 +00:00
mib1185
5c8dd9f2c3 apply suggestion 2026-02-15 15:42:49 +00:00
mib1185
b1d7e48a16 we only allow a valid URL now 2026-02-15 15:17:56 +00:00
mib1185
c687398689 add config entry migration test 2026-02-15 15:02:39 +00:00
mib1185
3bbe9e1c3d adjust existing tests 2026-02-15 14:53:49 +00:00
mib1185
35b767c6db rework config flow to accept teh full URL 2026-02-15 14:52:46 +00:00
9 changed files with 330 additions and 86 deletions

View File

@@ -3,14 +3,20 @@
from __future__ import annotations
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 DeviceEntry
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
@@ -67,6 +73,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: DeviceEntry
) -> bool:

View File

@@ -4,35 +4,54 @@ from __future__ import annotations
from collections.abc import Mapping
import ipaddress
from typing import Any, Self
from urllib.parse import urlparse
from typing import TYPE_CHECKING, Any, Self
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)
),
}
)
@@ -46,22 +65,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,
},
)
@@ -72,7 +94,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()
@@ -93,12 +118,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,39 +142,41 @@ 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()
def is_matching(self, other_flow: Self) -> bool:
"""Return True if other_flow is matching this flow."""
return other_flow._host == self._host # noqa: SLF001
return other_flow._url == self._url # noqa: SLF001
async def async_step_confirm(
self, user_input: dict[str, Any] | None = None
@@ -178,7 +206,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]
@@ -199,8 +228,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,
},
@@ -226,31 +254,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,
)

View File

@@ -15,8 +15,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"

View File

@@ -10,7 +10,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
@@ -63,6 +63,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],
)
try:

View File

@@ -1,4 +1,8 @@
{
"common": {
"data_desc_ssl_verify": "Whether to verify the SSL certificate when SSL encryption is used to connect to your FRITZ!Box.",
"data_desc_url": "The URL of your FRITZ!Box."
},
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
@@ -31,21 +35,25 @@
},
"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": "The hostname or IP address of your FRITZ!Box router."
"url": "[%key:component::fritzbox::common::data_desc_url%]",
"verify_ssl": "[%key:component::fritzbox::common::data_desc_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": "The hostname or IP address of your FRITZ!Box router."
"url": "[%key:component::fritzbox::common::data_desc_url%]",
"verify_ssl": "[%key:component::fritzbox::common::data_desc_ssl_verify%]"
},
"description": "Enter your FRITZ!Box information."
}

View File

@@ -29,9 +29,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:

View File

@@ -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,
}
]
}

View File

@@ -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
@@ -125,7 +141,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
@@ -149,7 +170,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
@@ -172,7 +198,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
@@ -192,51 +223,50 @@ 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_USERNAME] == "fake_user"
assert mock_config.data[CONF_PASSWORD] == "fake_pass"
result = await mock_config.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
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_USERNAME] == "fake_user"
assert mock_config.data[CONF_PASSWORD] == "fake_pass"
result = await mock_config.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
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"
@@ -244,16 +274,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(
@@ -287,9 +316,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"
@@ -309,10 +339,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"

View File

@@ -18,6 +18,7 @@ from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_USERNAME,
CONF_VERIFY_SSL,
EVENT_HOMEASSISTANT_STOP,
STATE_UNAVAILABLE,
UnitOfTemperature,
@@ -39,12 +40,18 @@ 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")
call(
host="http://10.0.0.1",
password="fake_pass",
user="fake_user",
ssl_verify=False,
)
]
@@ -86,6 +93,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)
@@ -145,6 +154,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)
@@ -170,6 +181,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)
@@ -209,6 +222,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)
@@ -281,6 +296,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(
@@ -302,6 +319,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(
@@ -315,3 +334,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