diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py
index 5740cf99f53..b51d01f0fd7 100644
--- a/homeassistant/components/huawei_lte/__init__.py
+++ b/homeassistant/components/huawei_lte/__init__.py
@@ -15,6 +15,7 @@ from huawei_lte_api.Client import Client
from huawei_lte_api.Connection import Connection
from huawei_lte_api.enums.device import ControlModeEnum
from huawei_lte_api.exceptions import (
+ LoginErrorInvalidCredentialsException,
ResponseErrorException,
ResponseErrorLoginRequiredException,
ResponseErrorNotSupportedException,
@@ -38,7 +39,7 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall
-from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
@@ -339,6 +340,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try:
connection = await hass.async_add_executor_job(get_connection)
+ except LoginErrorInvalidCredentialsException as ex:
+ raise ConfigEntryAuthFailed from ex
except Timeout as ex:
raise ConfigEntryNotReady from ex
diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py
index 3dfd38d6304..036eec37d44 100644
--- a/homeassistant/components/huawei_lte/config_flow.py
+++ b/homeassistant/components/huawei_lte/config_flow.py
@@ -1,6 +1,7 @@
"""Config flow for the Huawei LTE platform."""
from __future__ import annotations
+from collections.abc import Mapping
import logging
from typing import TYPE_CHECKING, Any
from urllib.parse import urlparse
@@ -89,6 +90,70 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors or {},
)
+ async def _async_show_reauth_form(
+ self,
+ user_input: dict[str, Any],
+ errors: dict[str, str] | None = None,
+ ) -> FlowResult:
+ return self.async_show_form(
+ step_id="reauth_confirm",
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_USERNAME, default=user_input.get(CONF_USERNAME) or ""
+ ): str,
+ vol.Optional(
+ CONF_PASSWORD, default=user_input.get(CONF_PASSWORD) or ""
+ ): str,
+ }
+ ),
+ errors=errors or {},
+ )
+
+ async def _try_connect(
+ self, user_input: dict[str, Any], errors: dict[str, str]
+ ) -> Connection | None:
+ """Try connecting with given data."""
+ username = user_input.get(CONF_USERNAME) or ""
+ password = user_input.get(CONF_PASSWORD) or ""
+
+ def _get_connection() -> Connection:
+ return Connection(
+ url=user_input[CONF_URL],
+ username=username,
+ password=password,
+ timeout=CONNECTION_TIMEOUT,
+ )
+
+ conn = None
+ try:
+ conn = await self.hass.async_add_executor_job(_get_connection)
+ except LoginErrorUsernameWrongException:
+ errors[CONF_USERNAME] = "incorrect_username"
+ except LoginErrorPasswordWrongException:
+ errors[CONF_PASSWORD] = "incorrect_password"
+ except LoginErrorUsernamePasswordWrongException:
+ errors[CONF_USERNAME] = "invalid_auth"
+ except LoginErrorUsernamePasswordOverrunException:
+ errors["base"] = "login_attempts_exceeded"
+ except ResponseErrorException:
+ _LOGGER.warning("Response error", exc_info=True)
+ errors["base"] = "response_error"
+ except Timeout:
+ _LOGGER.warning("Connection timeout", exc_info=True)
+ errors[CONF_URL] = "connection_timeout"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.warning("Unknown error connecting to device", exc_info=True)
+ errors[CONF_URL] = "unknown"
+ return conn
+
+ @staticmethod
+ def _logout(conn: Connection) -> None:
+ try:
+ conn.user_session.user.logout() # type: ignore[union-attr]
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.debug("Could not logout", exc_info=True)
+
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
@@ -108,25 +173,9 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
user_input=user_input, errors=errors
)
- def logout() -> None:
- try:
- conn.user_session.user.logout() # type: ignore[union-attr]
- except Exception: # pylint: disable=broad-except
- _LOGGER.debug("Could not logout", exc_info=True)
-
- def try_connect(user_input: dict[str, Any]) -> Connection:
- """Try connecting with given credentials."""
- username = user_input.get(CONF_USERNAME) or ""
- password = user_input.get(CONF_PASSWORD) or ""
- conn = Connection(
- user_input[CONF_URL],
- username=username,
- password=password,
- timeout=CONNECTION_TIMEOUT,
- )
- return conn
-
- def get_device_info() -> tuple[GetResponseType, GetResponseType]:
+ def get_device_info(
+ conn: Connection,
+ ) -> tuple[GetResponseType, GetResponseType]:
"""Get router info."""
client = Client(conn)
try:
@@ -147,33 +196,17 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
wlan_settings = {}
return device_info, wlan_settings
- try:
- conn = await self.hass.async_add_executor_job(try_connect, user_input)
- except LoginErrorUsernameWrongException:
- errors[CONF_USERNAME] = "incorrect_username"
- except LoginErrorPasswordWrongException:
- errors[CONF_PASSWORD] = "incorrect_password"
- except LoginErrorUsernamePasswordWrongException:
- errors[CONF_USERNAME] = "invalid_auth"
- except LoginErrorUsernamePasswordOverrunException:
- errors["base"] = "login_attempts_exceeded"
- except ResponseErrorException:
- _LOGGER.warning("Response error", exc_info=True)
- errors["base"] = "response_error"
- except Timeout:
- _LOGGER.warning("Connection timeout", exc_info=True)
- errors[CONF_URL] = "connection_timeout"
- except Exception: # pylint: disable=broad-except
- _LOGGER.warning("Unknown error connecting to device", exc_info=True)
- errors[CONF_URL] = "unknown"
+ conn = await self._try_connect(user_input, errors)
if errors:
- await self.hass.async_add_executor_job(logout)
return await self._async_show_user_form(
user_input=user_input, errors=errors
)
+ assert conn
- info, wlan_settings = await self.hass.async_add_executor_job(get_device_info)
- await self.hass.async_add_executor_job(logout)
+ info, wlan_settings = await self.hass.async_add_executor_job(
+ get_device_info, conn
+ )
+ await self.hass.async_add_executor_job(self._logout, conn)
user_input[CONF_MAC] = get_device_macs(info, wlan_settings)
@@ -228,6 +261,38 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
}
return await self._async_show_user_form(user_input)
+ async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
+ """Perform reauth upon an API authentication error."""
+ return await self.async_step_reauth_confirm()
+
+ async def async_step_reauth_confirm(
+ self, user_input: dict[str, Any] | None = None
+ ) -> FlowResult:
+ """Dialog that informs the user that reauth is required."""
+ entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
+ assert entry
+ if not user_input:
+ return await self._async_show_reauth_form(
+ user_input={
+ CONF_USERNAME: entry.data[CONF_USERNAME],
+ CONF_PASSWORD: entry.data[CONF_PASSWORD],
+ }
+ )
+
+ new_data = {**entry.data, **user_input}
+ errors: dict[str, str] = {}
+ conn = await self._try_connect(new_data, errors)
+ if conn:
+ await self.hass.async_add_executor_job(self._logout, conn)
+ if errors:
+ return await self._async_show_reauth_form(
+ user_input=user_input, errors=errors
+ )
+
+ self.hass.config_entries.async_update_entry(entry, data=new_data)
+ await self.hass.config_entries.async_reload(entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
+
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Huawei LTE options flow."""
diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json
index 910e0e132f1..473d8df3124 100644
--- a/homeassistant/components/huawei_lte/manifest.json
+++ b/homeassistant/components/huawei_lte/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/huawei_lte",
"requirements": [
- "huawei-lte-api==1.6.1",
+ "huawei-lte-api==1.6.3",
"stringcase==1.2.0",
"url-normalize==1.4.3"
],
diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json
index 0c1373192c5..8f6ec64491b 100644
--- a/homeassistant/components/huawei_lte/strings.json
+++ b/homeassistant/components/huawei_lte/strings.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "not_huawei_lte": "Not a Huawei LTE device"
+ "not_huawei_lte": "Not a Huawei LTE device",
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"connection_timeout": "Connection timeout",
@@ -15,6 +16,14 @@
},
"flow_title": "{name}",
"step": {
+ "reauth_confirm": {
+ "title": "[%key:common::config_flow::title::reauth%]",
+ "description": "Enter device access credentials.",
+ "data": {
+ "password": "[%key:common::config_flow::data::password%]",
+ "username": "[%key:common::config_flow::data::username%]"
+ }
+ },
"user": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
diff --git a/homeassistant/components/huawei_lte/translations/en.json b/homeassistant/components/huawei_lte/translations/en.json
index 5636d952b19..134a5372f71 100644
--- a/homeassistant/components/huawei_lte/translations/en.json
+++ b/homeassistant/components/huawei_lte/translations/en.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "not_huawei_lte": "Not a Huawei LTE device"
+ "not_huawei_lte": "Not a Huawei LTE device",
+ "reauth_successful": "Re-authentication was successful"
},
"error": {
"connection_timeout": "Connection timeout",
@@ -15,6 +16,14 @@
},
"flow_title": "{name}",
"step": {
+ "reauth_confirm": {
+ "data": {
+ "password": "Password",
+ "username": "Username"
+ },
+ "description": "Enter device access credentials.",
+ "title": "Reauthenticate Integration"
+ },
"user": {
"data": {
"password": "Password",
diff --git a/requirements_all.txt b/requirements_all.txt
index 64459d5fd5b..a8a081e5bd4 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -886,7 +886,7 @@ horimote==0.4.1
httplib2==0.20.4
# homeassistant.components.huawei_lte
-huawei-lte-api==1.6.1
+huawei-lte-api==1.6.3
# homeassistant.components.hydrawise
hydrawiser==0.2
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 36d0785d580..a90238ec882 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -663,7 +663,7 @@ homepluscontrol==0.0.5
httplib2==0.20.4
# homeassistant.components.huawei_lte
-huawei-lte-api==1.6.1
+huawei-lte-api==1.6.3
# homeassistant.components.hyperion
hyperion-py==0.7.5
diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py
index 84f66e8f0ab..56c177a3602 100644
--- a/tests/components/huawei_lte/test_config_flow.py
+++ b/tests/components/huawei_lte/test_config_flow.py
@@ -5,6 +5,7 @@ from unittest.mock import patch
from huawei_lte_api.enums.client import ResponseCodeEnum
from huawei_lte_api.enums.user import LoginErrorEnum, LoginStateEnum, PasswordTypeEnum
import pytest
+import requests.exceptions
from requests.exceptions import ConnectionError
from requests_mock import ANY
@@ -119,27 +120,66 @@ def login_requests_mock(requests_mock):
@pytest.mark.parametrize(
- ("code", "errors"),
+ ("request_outcome", "fixture_override", "errors"),
(
- (LoginErrorEnum.USERNAME_WRONG, {CONF_USERNAME: "incorrect_username"}),
- (LoginErrorEnum.PASSWORD_WRONG, {CONF_PASSWORD: "incorrect_password"}),
(
- LoginErrorEnum.USERNAME_PWD_WRONG,
+ {
+ "text": f"{LoginErrorEnum.USERNAME_WRONG}
",
+ },
+ {},
+ {CONF_USERNAME: "incorrect_username"},
+ ),
+ (
+ {
+ "text": f"{LoginErrorEnum.PASSWORD_WRONG}
",
+ },
+ {},
+ {CONF_PASSWORD: "incorrect_password"},
+ ),
+ (
+ {
+ "text": f"{LoginErrorEnum.USERNAME_PWD_WRONG}
",
+ },
+ {},
{CONF_USERNAME: "invalid_auth"},
),
- (LoginErrorEnum.USERNAME_PWD_OVERRUN, {"base": "login_attempts_exceeded"}),
- (ResponseCodeEnum.ERROR_SYSTEM_UNKNOWN, {"base": "response_error"}),
+ (
+ {
+ "text": f"{LoginErrorEnum.USERNAME_PWD_OVERRUN}
",
+ },
+ {},
+ {"base": "login_attempts_exceeded"},
+ ),
+ (
+ {
+ "text": f"{ResponseCodeEnum.ERROR_SYSTEM_UNKNOWN}
",
+ },
+ {},
+ {"base": "response_error"},
+ ),
+ ({}, {CONF_URL: "/foo/bar"}, {CONF_URL: "invalid_url"}),
+ (
+ {
+ "exc": requests.exceptions.Timeout,
+ },
+ {},
+ {CONF_URL: "connection_timeout"},
+ ),
),
)
-async def test_login_error(hass, login_requests_mock, code, errors):
+async def test_login_error(
+ hass, login_requests_mock, request_outcome, fixture_override, errors
+):
"""Test we show user form with appropriate error on response failure."""
login_requests_mock.request(
ANY,
f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login",
- text=f"{code}
",
+ **request_outcome,
)
result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT
+ DOMAIN,
+ context={"source": config_entries.SOURCE_USER},
+ data={**FIXTURE_USER_INPUT, **fixture_override},
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
@@ -170,7 +210,43 @@ async def test_success(hass, login_requests_mock):
assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD]
-async def test_ssdp(hass):
+@pytest.mark.parametrize(
+ ("upnp_data", "expected_result"),
+ (
+ (
+ {
+ ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi",
+ ssdp.ATTR_UPNP_SERIAL: "00000000",
+ },
+ {
+ "type": data_entry_flow.FlowResultType.FORM,
+ "step_id": "user",
+ "errors": {},
+ },
+ ),
+ (
+ {
+ ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi",
+ # No ssdp.ATTR_UPNP_SERIAL
+ },
+ {
+ "type": data_entry_flow.FlowResultType.FORM,
+ "step_id": "user",
+ "errors": {},
+ },
+ ),
+ (
+ {
+ ssdp.ATTR_UPNP_FRIENDLY_NAME: "Some other device",
+ },
+ {
+ "type": data_entry_flow.FlowResultType.ABORT,
+ "reason": "not_huawei_lte",
+ },
+ ),
+ ),
+)
+async def test_ssdp(hass, upnp_data, expected_result):
"""Test SSDP discovery initiates config properly."""
url = "http://192.168.100.1/"
context = {"source": config_entries.SOURCE_SSDP}
@@ -183,21 +259,93 @@ async def test_ssdp(hass):
ssdp_location="http://192.168.100.1:60957/rootDesc.xml",
upnp={
ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1",
- ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi",
ssdp.ATTR_UPNP_MANUFACTURER: "Huawei",
ssdp.ATTR_UPNP_MANUFACTURER_URL: "http://www.huawei.com/",
ssdp.ATTR_UPNP_MODEL_NAME: "Huawei router",
ssdp.ATTR_UPNP_MODEL_NUMBER: "12345678",
ssdp.ATTR_UPNP_PRESENTATION_URL: url,
- ssdp.ATTR_UPNP_SERIAL: "00000000",
ssdp.ATTR_UPNP_UDN: "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
+ **upnp_data,
},
),
)
+ for k, v in expected_result.items():
+ assert result[k] == v
+ if result.get("data_schema"):
+ result["data_schema"]({})[CONF_URL] == url
+
+
+@pytest.mark.parametrize(
+ ("login_response_text", "expected_result", "expected_entry_data"),
+ (
+ (
+ "OK",
+ {
+ "type": data_entry_flow.FlowResultType.ABORT,
+ "reason": "reauth_successful",
+ },
+ FIXTURE_USER_INPUT,
+ ),
+ (
+ f"{LoginErrorEnum.PASSWORD_WRONG}
",
+ {
+ "type": data_entry_flow.FlowResultType.FORM,
+ "errors": {CONF_PASSWORD: "incorrect_password"},
+ "step_id": "reauth_confirm",
+ },
+ {**FIXTURE_USER_INPUT, CONF_PASSWORD: "invalid-password"},
+ ),
+ ),
+)
+async def test_reauth(
+ hass, login_requests_mock, login_response_text, expected_result, expected_entry_data
+):
+ """Test reauth."""
+ mock_entry_data = {**FIXTURE_USER_INPUT, CONF_PASSWORD: "invalid-password"}
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id=FIXTURE_UNIQUE_ID,
+ data=mock_entry_data,
+ title="Reauth canary",
+ )
+ entry.add_to_hass(hass)
+
+ context = {
+ "source": config_entries.SOURCE_REAUTH,
+ "unique_id": entry.unique_id,
+ "entry_id": entry.entry_id,
+ }
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context=context, data=entry.data
+ )
+
assert result["type"] == data_entry_flow.FlowResultType.FORM
- assert result["step_id"] == "user"
- assert result["data_schema"]({})[CONF_URL] == url
+ assert result["step_id"] == "reauth_confirm"
+ assert result["data_schema"]({}) == {
+ CONF_USERNAME: mock_entry_data[CONF_USERNAME],
+ CONF_PASSWORD: mock_entry_data[CONF_PASSWORD],
+ }
+ assert not result["errors"]
+
+ login_requests_mock.request(
+ ANY,
+ f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login",
+ text=login_response_text,
+ )
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: FIXTURE_USER_INPUT[CONF_USERNAME],
+ CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD],
+ },
+ )
+ await hass.async_block_till_done()
+
+ for k, v in expected_result.items():
+ assert result[k] == v
+ for k, v in expected_entry_data.items():
+ assert entry.data[k] == v
async def test_options(hass):