mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add QNAP QSW DHCP discovery (#73130)
* qnap_qsw: add DHCP discovery Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * qnap_qsw: config_flow: add async_step_dhcp Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * qnap_qsw: config_flow: lower DHCP logging Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * tests: qnap_qsw: fix copy & paste Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * qnap_qsw: dhcp: introduce changes suggested by @bdraco Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * Update homeassistant/components/qnap_qsw/config_flow.py Co-authored-by: J. Nick Koston <nick@koston.org> * qnap_qsw: async_step_user: disable raising on progress Allows async_step_user to win over a discovery. Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
24bf42cfbe
commit
be6c2554dd
@ -1,6 +1,7 @@
|
|||||||
"""Config flow for QNAP QSW."""
|
"""Config flow for QNAP QSW."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aioqsw.exceptions import LoginError, QswError
|
from aioqsw.exceptions import LoginError, QswError
|
||||||
@ -8,6 +9,7 @@ from aioqsw.localapi import ConnectionOptions, QnapQswApi
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components import dhcp
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
|
||||||
from homeassistant.data_entry_flow import AbortFlow, FlowResult
|
from homeassistant.data_entry_flow import AbortFlow, FlowResult
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
@ -15,10 +17,15 @@ from homeassistant.helpers.device_registry import format_mac
|
|||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle config flow for a QNAP QSW device."""
|
"""Handle config flow for a QNAP QSW device."""
|
||||||
|
|
||||||
|
_discovered_mac: str | None = None
|
||||||
|
_discovered_url: str | None = None
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> FlowResult:
|
) -> FlowResult:
|
||||||
@ -46,7 +53,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
if mac is None:
|
if mac is None:
|
||||||
raise AbortFlow("invalid_id")
|
raise AbortFlow("invalid_id")
|
||||||
|
|
||||||
await self.async_set_unique_id(format_mac(mac))
|
await self.async_set_unique_id(format_mac(mac), raise_on_progress=False)
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
title = f"QNAP {system_board.get_product()} {mac}"
|
title = f"QNAP {system_board.get_product()} {mac}"
|
||||||
@ -63,3 +70,61 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
),
|
),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
|
||||||
|
"""Handle DHCP discovery."""
|
||||||
|
self._discovered_url = f"http://{discovery_info.ip}"
|
||||||
|
self._discovered_mac = discovery_info.macaddress
|
||||||
|
|
||||||
|
_LOGGER.debug("DHCP discovery detected QSW: %s", self._discovered_mac)
|
||||||
|
|
||||||
|
mac = format_mac(self._discovered_mac)
|
||||||
|
options = ConnectionOptions(self._discovered_url, "", "")
|
||||||
|
qsw = QnapQswApi(aiohttp_client.async_get_clientsession(self.hass), options)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await qsw.get_live()
|
||||||
|
except QswError as err:
|
||||||
|
raise AbortFlow("cannot_connect") from err
|
||||||
|
|
||||||
|
await self.async_set_unique_id(format_mac(mac))
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
return await self.async_step_discovered_connection()
|
||||||
|
|
||||||
|
async def async_step_discovered_connection(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Confirm discovery."""
|
||||||
|
errors = {}
|
||||||
|
assert self._discovered_url is not None
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
username = user_input[CONF_USERNAME]
|
||||||
|
password = user_input[CONF_PASSWORD]
|
||||||
|
|
||||||
|
qsw = QnapQswApi(
|
||||||
|
aiohttp_client.async_get_clientsession(self.hass),
|
||||||
|
ConnectionOptions(self._discovered_url, username, password),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
system_board = await qsw.validate()
|
||||||
|
except LoginError:
|
||||||
|
errors[CONF_PASSWORD] = "invalid_auth"
|
||||||
|
except QswError:
|
||||||
|
errors[CONF_URL] = "cannot_connect"
|
||||||
|
else:
|
||||||
|
title = f"QNAP {system_board.get_product()} {self._discovered_mac}"
|
||||||
|
return self.async_create_entry(title=title, data=user_input)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="discovered_connection",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_USERNAME): str,
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
@ -6,5 +6,10 @@
|
|||||||
"requirements": ["aioqsw==0.1.0"],
|
"requirements": ["aioqsw==0.1.0"],
|
||||||
"codeowners": ["@Noltari"],
|
"codeowners": ["@Noltari"],
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioqsw"]
|
"loggers": ["aioqsw"],
|
||||||
|
"dhcp": [
|
||||||
|
{
|
||||||
|
"macaddress": "245EBE*"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -74,6 +74,7 @@ DHCP: list[dict[str, str | bool]] = [
|
|||||||
{'domain': 'oncue', 'hostname': 'kohlergen*', 'macaddress': '00146F*'},
|
{'domain': 'oncue', 'hostname': 'kohlergen*', 'macaddress': '00146F*'},
|
||||||
{'domain': 'overkiz', 'hostname': 'gateway*', 'macaddress': 'F8811A*'},
|
{'domain': 'overkiz', 'hostname': 'gateway*', 'macaddress': 'F8811A*'},
|
||||||
{'domain': 'powerwall', 'hostname': '1118431-*'},
|
{'domain': 'powerwall', 'hostname': '1118431-*'},
|
||||||
|
{'domain': 'qnap_qsw', 'macaddress': '245EBE*'},
|
||||||
{'domain': 'rachio', 'hostname': 'rachio-*', 'macaddress': '009D6B*'},
|
{'domain': 'rachio', 'hostname': 'rachio-*', 'macaddress': '009D6B*'},
|
||||||
{'domain': 'rachio', 'hostname': 'rachio-*', 'macaddress': 'F0038C*'},
|
{'domain': 'rachio', 'hostname': 'rachio-*', 'macaddress': 'F0038C*'},
|
||||||
{'domain': 'rachio', 'hostname': 'rachio-*', 'macaddress': '74C63B*'},
|
{'domain': 'rachio', 'hostname': 'rachio-*', 'macaddress': '74C63B*'},
|
||||||
|
@ -5,7 +5,8 @@ from unittest.mock import MagicMock, patch
|
|||||||
from aioqsw.const import API_MAC_ADDR, API_PRODUCT, API_RESULT
|
from aioqsw.const import API_MAC_ADDR, API_PRODUCT, API_RESULT
|
||||||
from aioqsw.exceptions import LoginError, QswError
|
from aioqsw.exceptions import LoginError, QswError
|
||||||
|
|
||||||
from homeassistant import data_entry_flow
|
from homeassistant import config_entries, data_entry_flow
|
||||||
|
from homeassistant.components import dhcp
|
||||||
from homeassistant.components.qnap_qsw.const import DOMAIN
|
from homeassistant.components.qnap_qsw.const import DOMAIN
|
||||||
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
|
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
|
||||||
@ -16,6 +17,16 @@ from .util import CONFIG, LIVE_MOCK, SYSTEM_BOARD_MOCK, USERS_LOGIN_MOCK
|
|||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
DHCP_SERVICE_INFO = dhcp.DhcpServiceInfo(
|
||||||
|
hostname="qsw-m408-4c",
|
||||||
|
ip="192.168.1.200",
|
||||||
|
macaddress="245EBE000000",
|
||||||
|
)
|
||||||
|
|
||||||
|
TEST_PASSWORD = "test-password"
|
||||||
|
TEST_URL = "test-url"
|
||||||
|
TEST_USERNAME = "test-username"
|
||||||
|
|
||||||
|
|
||||||
async def test_form(hass: HomeAssistant) -> None:
|
async def test_form(hass: HomeAssistant) -> None:
|
||||||
"""Test that the form is served with valid input."""
|
"""Test that the form is served with valid input."""
|
||||||
@ -134,3 +145,127 @@ async def test_login_error(hass: HomeAssistant):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result["errors"] == {CONF_PASSWORD: "invalid_auth"}
|
assert result["errors"] == {CONF_PASSWORD: "invalid_auth"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_dhcp_flow(hass: HomeAssistant) -> None:
|
||||||
|
"""Test that DHCP discovery works."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.qnap_qsw.QnapQswApi.get_live",
|
||||||
|
return_value=LIVE_MOCK,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
data=DHCP_SERVICE_INFO,
|
||||||
|
context={"source": config_entries.SOURCE_DHCP},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "discovered_connection"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.qnap_qsw.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup_entry, patch(
|
||||||
|
"homeassistant.components.qnap_qsw.QnapQswApi.get_live",
|
||||||
|
return_value=LIVE_MOCK,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.qnap_qsw.QnapQswApi.get_system_board",
|
||||||
|
return_value=SYSTEM_BOARD_MOCK,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.qnap_qsw.QnapQswApi.post_users_login",
|
||||||
|
return_value=USERS_LOGIN_MOCK,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_USERNAME: TEST_USERNAME,
|
||||||
|
CONF_PASSWORD: TEST_PASSWORD,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "create_entry"
|
||||||
|
assert result2["data"] == {
|
||||||
|
CONF_USERNAME: TEST_USERNAME,
|
||||||
|
CONF_PASSWORD: TEST_PASSWORD,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_dhcp_flow_error(hass: HomeAssistant) -> None:
|
||||||
|
"""Test that DHCP discovery fails."""
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.qnap_qsw.QnapQswApi.get_live",
|
||||||
|
side_effect=QswError,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
data=DHCP_SERVICE_INFO,
|
||||||
|
context={"source": config_entries.SOURCE_DHCP},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "cannot_connect"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_dhcp_connection_error(hass: HomeAssistant):
|
||||||
|
"""Test DHCP connection to host error."""
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.qnap_qsw.QnapQswApi.get_live",
|
||||||
|
return_value=LIVE_MOCK,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
data=DHCP_SERVICE_INFO,
|
||||||
|
context={"source": config_entries.SOURCE_DHCP},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "discovered_connection"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.qnap_qsw.QnapQswApi.validate",
|
||||||
|
side_effect=QswError,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_USERNAME: TEST_USERNAME,
|
||||||
|
CONF_PASSWORD: TEST_PASSWORD,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["errors"] == {CONF_URL: "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_dhcp_login_error(hass: HomeAssistant):
|
||||||
|
"""Test DHCP login error."""
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.qnap_qsw.QnapQswApi.get_live",
|
||||||
|
return_value=LIVE_MOCK,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
data=DHCP_SERVICE_INFO,
|
||||||
|
context={"source": config_entries.SOURCE_DHCP},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "discovered_connection"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.qnap_qsw.QnapQswApi.validate",
|
||||||
|
side_effect=LoginError,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_USERNAME: TEST_USERNAME,
|
||||||
|
CONF_PASSWORD: TEST_PASSWORD,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["errors"] == {CONF_PASSWORD: "invalid_auth"}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user