mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Support Huawei LTE SSDP discovery (#28214)
* Support Huawei LTE SSDP discovery * Avoid KeyError on simultaneous user initiated flow Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io> * Format code * Add already configured check * Initialize context in test flows * Move deviceType match to manifest * Update generated.ssdp * Add SSDP config flow test case * Remove stale debug print from tests
This commit is contained in:
parent
f3ea44cd92
commit
6a7b5657ac
@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "This device is already configured"
|
"already_configured": "This device has already been configured",
|
||||||
|
"already_in_progress": "This device is already being configured",
|
||||||
|
"not_huawei_lte": "Not a Huawei LTE device"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"connection_failed": "Connection failed",
|
"connection_failed": "Connection failed",
|
||||||
|
@ -19,6 +19,7 @@ from url_normalize import url_normalize
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.ssdp import ATTR_HOST, ATTR_NAME, ATTR_PRESENTATIONURL
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_URL, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_URL, CONF_USERNAME
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from .const import CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME
|
from .const import CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME
|
||||||
@ -52,7 +53,14 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
(
|
(
|
||||||
(
|
(
|
||||||
vol.Required(
|
vol.Required(
|
||||||
CONF_URL, default=user_input.get(CONF_URL, "")
|
CONF_URL,
|
||||||
|
default=user_input.get(
|
||||||
|
CONF_URL,
|
||||||
|
# https://github.com/PyCQA/pylint/issues/3167
|
||||||
|
self.context.get( # pylint: disable=no-member
|
||||||
|
CONF_URL, ""
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
str,
|
str,
|
||||||
),
|
),
|
||||||
@ -78,6 +86,14 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle import initiated config flow."""
|
"""Handle import initiated config flow."""
|
||||||
return await self.async_step_user(user_input)
|
return await self.async_step_user(user_input)
|
||||||
|
|
||||||
|
def _already_configured(self, user_input):
|
||||||
|
"""See if we already have a router matching user input configured."""
|
||||||
|
existing_urls = {
|
||||||
|
url_normalize(entry.data[CONF_URL], default_scheme="http")
|
||||||
|
for entry in self._async_current_entries()
|
||||||
|
}
|
||||||
|
return user_input[CONF_URL] in existing_urls
|
||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
async def async_step_user(self, user_input=None):
|
||||||
"""Handle user initiated config flow."""
|
"""Handle user initiated config flow."""
|
||||||
if user_input is None:
|
if user_input is None:
|
||||||
@ -95,12 +111,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
user_input=user_input, errors=errors
|
user_input=user_input, errors=errors
|
||||||
)
|
)
|
||||||
|
|
||||||
# See if we already have a router configured with this URL
|
if self._already_configured(user_input):
|
||||||
existing_urls = { # existing entries
|
|
||||||
url_normalize(entry.data[CONF_URL], default_scheme="http")
|
|
||||||
for entry in self._async_current_entries()
|
|
||||||
}
|
|
||||||
if user_input[CONF_URL] in existing_urls:
|
|
||||||
return self.async_abort(reason="already_configured")
|
return self.async_abort(reason="already_configured")
|
||||||
|
|
||||||
conn = None
|
conn = None
|
||||||
@ -194,6 +205,31 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
return self.async_create_entry(title=title, data=user_input)
|
return self.async_create_entry(title=title, data=user_input)
|
||||||
|
|
||||||
|
async def async_step_ssdp(self, discovery_info):
|
||||||
|
"""Handle SSDP initiated config flow."""
|
||||||
|
# Attempt to distinguish from other non-LTE Huawei router devices, at least
|
||||||
|
# some ones we are interested in have "Mobile Wi-Fi" friendlyName.
|
||||||
|
if "mobile" not in discovery_info.get(ATTR_NAME, "").lower():
|
||||||
|
return self.async_abort(reason="not_huawei_lte")
|
||||||
|
|
||||||
|
# https://github.com/PyCQA/pylint/issues/3167
|
||||||
|
url = self.context[CONF_URL] = url_normalize( # pylint: disable=no-member
|
||||||
|
discovery_info.get(
|
||||||
|
ATTR_PRESENTATIONURL, f"http://{discovery_info[ATTR_HOST]}/"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if any(
|
||||||
|
url == flow["context"].get(CONF_URL) for flow in self._async_in_progress()
|
||||||
|
):
|
||||||
|
return self.async_abort(reason="already_in_progress")
|
||||||
|
|
||||||
|
user_input = {CONF_URL: url}
|
||||||
|
if self._already_configured(user_input):
|
||||||
|
return self.async_abort(reason="already_configured")
|
||||||
|
|
||||||
|
return await self._async_show_user_form(user_input)
|
||||||
|
|
||||||
|
|
||||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
"""Huawei LTE options flow."""
|
"""Huawei LTE options flow."""
|
||||||
|
@ -9,6 +9,12 @@
|
|||||||
"stringcase==1.2.0",
|
"stringcase==1.2.0",
|
||||||
"url-normalize==1.4.1"
|
"url-normalize==1.4.1"
|
||||||
],
|
],
|
||||||
|
"ssdp": [
|
||||||
|
{
|
||||||
|
"deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
||||||
|
"manufacturer": "Huawei"
|
||||||
|
}
|
||||||
|
],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
"@scop"
|
"@scop"
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "This device is already configured"
|
"already_configured": "This device has already been configured",
|
||||||
|
"already_in_progress": "This device is already being configured",
|
||||||
|
"not_huawei_lte": "Not a Huawei LTE device"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"connection_failed": "Connection failed",
|
"connection_failed": "Connection failed",
|
||||||
|
@ -16,6 +16,12 @@ SSDP = {
|
|||||||
"st": "urn:schemas-denon-com:device:ACT-Denon:1"
|
"st": "urn:schemas-denon-com:device:ACT-Denon:1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"huawei_lte": [
|
||||||
|
{
|
||||||
|
"deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
||||||
|
"manufacturer": "Huawei"
|
||||||
|
}
|
||||||
|
],
|
||||||
"hue": [
|
"hue": [
|
||||||
{
|
{
|
||||||
"manufacturer": "Royal Philips Electronics"
|
"manufacturer": "Royal Philips Electronics"
|
||||||
|
@ -10,6 +10,21 @@ from homeassistant import data_entry_flow
|
|||||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_URL
|
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_URL
|
||||||
from homeassistant.components.huawei_lte.const import DOMAIN
|
from homeassistant.components.huawei_lte.const import DOMAIN
|
||||||
from homeassistant.components.huawei_lte.config_flow import ConfigFlowHandler
|
from homeassistant.components.huawei_lte.config_flow import ConfigFlowHandler
|
||||||
|
from homeassistant.components.ssdp import (
|
||||||
|
ATTR_HOST,
|
||||||
|
ATTR_MANUFACTURER,
|
||||||
|
ATTR_MANUFACTURERURL,
|
||||||
|
ATTR_MODEL_NAME,
|
||||||
|
ATTR_MODEL_NUMBER,
|
||||||
|
ATTR_NAME,
|
||||||
|
ATTR_PORT,
|
||||||
|
ATTR_PRESENTATIONURL,
|
||||||
|
ATTR_SERIAL,
|
||||||
|
ATTR_ST,
|
||||||
|
ATTR_UDN,
|
||||||
|
ATTR_UPNP_DEVICE_TYPE,
|
||||||
|
)
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
@ -20,21 +35,26 @@ FIXTURE_USER_INPUT = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_show_set_form(hass):
|
@pytest.fixture
|
||||||
"""Test that the setup form is served."""
|
def flow(hass):
|
||||||
|
"""Get flow to test."""
|
||||||
flow = ConfigFlowHandler()
|
flow = ConfigFlowHandler()
|
||||||
flow.hass = hass
|
flow.hass = hass
|
||||||
|
flow.context = {}
|
||||||
|
return flow
|
||||||
|
|
||||||
|
|
||||||
|
async def test_show_set_form(flow):
|
||||||
|
"""Test that the setup form is served."""
|
||||||
result = await flow.async_step_user(user_input=None)
|
result = await flow.async_step_user(user_input=None)
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
|
||||||
async def test_urlize_plain_host(hass, requests_mock):
|
async def test_urlize_plain_host(flow, requests_mock):
|
||||||
"""Test that plain host or IP gets converted to a URL."""
|
"""Test that plain host or IP gets converted to a URL."""
|
||||||
requests_mock.request(ANY, ANY, exc=ConnectionError())
|
requests_mock.request(ANY, ANY, exc=ConnectionError())
|
||||||
flow = ConfigFlowHandler()
|
|
||||||
flow.hass = hass
|
|
||||||
host = "192.168.100.1"
|
host = "192.168.100.1"
|
||||||
user_input = {**FIXTURE_USER_INPUT, CONF_URL: host}
|
user_input = {**FIXTURE_USER_INPUT, CONF_URL: host}
|
||||||
result = await flow.async_step_user(user_input=user_input)
|
result = await flow.async_step_user(user_input=user_input)
|
||||||
@ -44,14 +64,12 @@ async def test_urlize_plain_host(hass, requests_mock):
|
|||||||
assert user_input[CONF_URL] == f"http://{host}/"
|
assert user_input[CONF_URL] == f"http://{host}/"
|
||||||
|
|
||||||
|
|
||||||
async def test_already_configured(hass):
|
async def test_already_configured(flow):
|
||||||
"""Test we reject already configured devices."""
|
"""Test we reject already configured devices."""
|
||||||
MockConfigEntry(
|
MockConfigEntry(
|
||||||
domain=DOMAIN, data=FIXTURE_USER_INPUT, title="Already configured"
|
domain=DOMAIN, data=FIXTURE_USER_INPUT, title="Already configured"
|
||||||
).add_to_hass(hass)
|
).add_to_hass(flow.hass)
|
||||||
|
|
||||||
flow = ConfigFlowHandler()
|
|
||||||
flow.hass = hass
|
|
||||||
# Tweak URL a bit to check that doesn't fail duplicate detection
|
# Tweak URL a bit to check that doesn't fail duplicate detection
|
||||||
result = await flow.async_step_user(
|
result = await flow.async_step_user(
|
||||||
user_input={
|
user_input={
|
||||||
@ -64,12 +82,10 @@ async def test_already_configured(hass):
|
|||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
async def test_connection_error(hass, requests_mock):
|
async def test_connection_error(flow, requests_mock):
|
||||||
"""Test we show user form on connection error."""
|
"""Test we show user form on connection error."""
|
||||||
|
|
||||||
requests_mock.request(ANY, ANY, exc=ConnectionError())
|
requests_mock.request(ANY, ANY, exc=ConnectionError())
|
||||||
flow = ConfigFlowHandler()
|
|
||||||
flow.hass = hass
|
|
||||||
result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT)
|
result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT)
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
@ -107,15 +123,13 @@ def login_requests_mock(requests_mock):
|
|||||||
(ResponseCodeEnum.ERROR_SYSTEM_UNKNOWN, {"base": "response_error"}),
|
(ResponseCodeEnum.ERROR_SYSTEM_UNKNOWN, {"base": "response_error"}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def test_login_error(hass, login_requests_mock, code, errors):
|
async def test_login_error(flow, login_requests_mock, code, errors):
|
||||||
"""Test we show user form with appropriate error on response failure."""
|
"""Test we show user form with appropriate error on response failure."""
|
||||||
login_requests_mock.request(
|
login_requests_mock.request(
|
||||||
ANY,
|
ANY,
|
||||||
f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login",
|
f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login",
|
||||||
text=f"<error><code>{code}</code><message/></error>",
|
text=f"<error><code>{code}</code><message/></error>",
|
||||||
)
|
)
|
||||||
flow = ConfigFlowHandler()
|
|
||||||
flow.hass = hass
|
|
||||||
result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT)
|
result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT)
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
@ -123,18 +137,41 @@ async def test_login_error(hass, login_requests_mock, code, errors):
|
|||||||
assert result["errors"] == errors
|
assert result["errors"] == errors
|
||||||
|
|
||||||
|
|
||||||
async def test_success(hass, login_requests_mock):
|
async def test_success(flow, login_requests_mock):
|
||||||
"""Test successful flow provides entry creation data."""
|
"""Test successful flow provides entry creation data."""
|
||||||
login_requests_mock.request(
|
login_requests_mock.request(
|
||||||
ANY,
|
ANY,
|
||||||
f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login",
|
f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login",
|
||||||
text=f"<response>OK</response>",
|
text=f"<response>OK</response>",
|
||||||
)
|
)
|
||||||
flow = ConfigFlowHandler()
|
|
||||||
flow.hass = hass
|
|
||||||
result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT)
|
result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT)
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL]
|
assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL]
|
||||||
assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME]
|
assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME]
|
||||||
assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD]
|
assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ssdp(flow):
|
||||||
|
"""Test SSDP discovery initiates config properly."""
|
||||||
|
url = "http://192.168.100.1/"
|
||||||
|
result = await flow.async_step_ssdp(
|
||||||
|
discovery_info={
|
||||||
|
ATTR_ST: "upnp:rootdevice",
|
||||||
|
ATTR_PORT: 60957,
|
||||||
|
ATTR_HOST: "192.168.100.1",
|
||||||
|
ATTR_MANUFACTURER: "Huawei",
|
||||||
|
ATTR_MANUFACTURERURL: "http://www.huawei.com/",
|
||||||
|
ATTR_MODEL_NAME: "Huawei router",
|
||||||
|
ATTR_MODEL_NUMBER: "12345678",
|
||||||
|
ATTR_NAME: "Mobile Wi-Fi",
|
||||||
|
ATTR_PRESENTATIONURL: url,
|
||||||
|
ATTR_SERIAL: "00000000",
|
||||||
|
ATTR_UDN: "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
|
||||||
|
ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert flow.context[CONF_URL] == url
|
||||||
|
Loading…
x
Reference in New Issue
Block a user