1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-02 00:20:30 +01:00

Add reconfiguration flow to QNAP (#166064)

Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
Paul Laffitte
2026-03-24 21:30:43 +01:00
committed by GitHub
parent cbe767c9c5
commit c35a6dc044
3 changed files with 186 additions and 29 deletions

View File

@@ -47,33 +47,43 @@ class QnapConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
async def _async_validate_input(
self, user_input: dict[str, Any]
) -> tuple[dict[str, str], dict[str, Any] | None]:
"""Validate user input by connecting to the QNAP device."""
errors: dict[str, str] = {}
host = user_input[CONF_HOST]
protocol = "https" if user_input[CONF_SSL] else "http"
api = QNAPStats(
host=f"{protocol}://{host}",
port=user_input[CONF_PORT],
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
verify_ssl=user_input[CONF_VERIFY_SSL],
timeout=DEFAULT_TIMEOUT,
)
try:
stats = await self.hass.async_add_executor_job(api.get_system_stats)
except ConnectTimeout:
errors["base"] = "cannot_connect"
except TypeError:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected error")
errors["base"] = "unknown"
else:
return errors, stats
return errors, None
async def async_step_user(
self,
user_input: dict[str, Any] | None = None,
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
errors = {}
errors: dict[str, str] = {}
if user_input is not None:
host = user_input[CONF_HOST]
protocol = "https" if user_input[CONF_SSL] else "http"
api = QNAPStats(
host=f"{protocol}://{host}",
port=user_input[CONF_PORT],
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
verify_ssl=user_input[CONF_VERIFY_SSL],
timeout=DEFAULT_TIMEOUT,
)
try:
stats = await self.hass.async_add_executor_job(api.get_system_stats)
except ConnectTimeout:
errors["base"] = "cannot_connect"
except TypeError:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected error")
errors["base"] = "unknown"
else:
errors, stats = await self._async_validate_input(user_input)
if not errors and stats is not None:
unique_id = stats["system"]["serial_number"]
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
@@ -85,3 +95,33 @@ class QnapConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=self.add_suggested_values_to_schema(DATA_SCHEMA, user_input),
errors=errors,
)
async def async_step_reconfigure(
self,
user_input: dict[str, Any] | None = None,
) -> ConfigFlowResult:
"""Handle reconfiguration of the integration."""
errors: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()
if user_input is not None:
errors, stats = await self._async_validate_input(user_input)
if not errors and stats is not None:
unique_id = stats["system"]["serial_number"]
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
reconfigure_entry,
data_updates=user_input,
)
suggested_values: dict[str, Any] = dict(user_input or reconfigure_entry.data)
suggested_values.pop(CONF_PASSWORD, None)
return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
DATA_SCHEMA,
suggested_values,
),
errors=errors,
)

View File

@@ -1,11 +1,31 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"unique_id_mismatch": "The device serial number does not match the original device. Please make sure you are connecting to the same QNAP NAS."
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reconfigure": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"password": "[%key:common::config_flow::data::password%]",
"port": "[%key:common::config_flow::data::port%]",
"ssl": "[%key:common::config_flow::data::ssl%]",
"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 QNAP device."
},
"description": "Update the connection settings for your QNAP NAS.",
"title": "Reconfigure QNAP connection"
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",

View File

@@ -18,7 +18,9 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from .conftest import TEST_HOST, TEST_PASSWORD, TEST_USERNAME
from .conftest import TEST_HOST, TEST_PASSWORD, TEST_SERIAL, TEST_USERNAME
from tests.common import MockConfigEntry
STANDARD_CONFIG = {
CONF_USERNAME: TEST_USERNAME,
@@ -26,6 +28,15 @@ STANDARD_CONFIG = {
CONF_HOST: TEST_HOST,
}
ENTRY_DATA = {
CONF_HOST: TEST_HOST,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_SSL: const.DEFAULT_SSL,
CONF_VERIFY_SSL: const.DEFAULT_VERIFY_SSL,
CONF_PORT: const.DEFAULT_PORT,
}
pytestmark = pytest.mark.usefixtures("mock_setup_entry", "qnap_connect")
@@ -78,11 +89,97 @@ async def test_config_flow(hass: HomeAssistant, qnap_connect: MagicMock) -> None
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Test NAS name"
assert result["data"] == {
CONF_HOST: "1.2.3.4",
CONF_USERNAME: "admin",
CONF_PASSWORD: "password",
CONF_SSL: const.DEFAULT_SSL,
CONF_VERIFY_SSL: const.DEFAULT_VERIFY_SSL,
CONF_PORT: const.DEFAULT_PORT,
assert result["data"] == ENTRY_DATA
async def test_reconfigure_flow(hass: HomeAssistant, qnap_connect: MagicMock) -> None:
"""Test reconfigure flow updates the config entry."""
entry = MockConfigEntry(
domain=const.DOMAIN,
unique_id=TEST_SERIAL,
data=ENTRY_DATA,
)
entry.add_to_hass(hass)
result = await entry.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"],
{**STANDARD_CONFIG, CONF_HOST: "5.6.7.8"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"
assert entry.data[CONF_HOST] == "5.6.7.8"
@pytest.mark.parametrize(
("side_effect", "error"),
[
(ConnectTimeout("Test error"), "cannot_connect"),
(TypeError("Test error"), "invalid_auth"),
(Exception("Test error"), "unknown"),
],
)
async def test_reconfigure_errors(
hass: HomeAssistant,
qnap_connect: MagicMock,
side_effect: Exception,
error: str,
) -> None:
"""Test reconfigure flow shows error on various exceptions."""
entry = MockConfigEntry(
domain=const.DOMAIN,
unique_id=TEST_SERIAL,
data=ENTRY_DATA,
)
entry.add_to_hass(hass)
result = await entry.start_reconfigure_flow(hass)
qnap_connect.get_system_stats.side_effect = side_effect
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
STANDARD_CONFIG,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
assert result["errors"] == {"base": error}
qnap_connect.get_system_stats.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
STANDARD_CONFIG,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"
async def test_reconfigure_unique_id_mismatch(
hass: HomeAssistant, qnap_connect: MagicMock
) -> None:
"""Test reconfigure aborts when serial number doesn't match."""
entry = MockConfigEntry(
domain=const.DOMAIN,
unique_id=TEST_SERIAL,
data=ENTRY_DATA,
)
entry.add_to_hass(hass)
qnap_connect.get_system_stats.return_value = {
"system": {"serial_number": "DIFFERENT_SERIAL", "name": "Other NAS"}
}
result = await entry.start_reconfigure_flow(hass)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
STANDARD_CONFIG,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "unique_id_mismatch"
assert entry.data[CONF_HOST] == TEST_HOST