Xiaomi Miio appropriatly raise ConfigEntryAuthFailed/ConfigEntryNotReady (#54696)
* Add reties to cloud login * push to version 0.4 of micloud * distinguish between authentication error and socket errors * raise from error * Update homeassistant/components/xiaomi_miio/gateway.py Co-authored-by: Franck Nijhof <git@frenck.dev> * move ConfigEntryNotReady to connect function * remove unused import * also add ConfigEntryNotReady for device * catch exceptions in config flow * fixes * bring tests back to 100% * add missing catch exception * add test * fix black * Update homeassistant/components/xiaomi_miio/device.py Co-authored-by: Teemu R. <tpr@iki.fi> * Update homeassistant/components/xiaomi_miio/gateway.py Co-authored-by: Teemu R. <tpr@iki.fi> * Update tests/components/xiaomi_miio/test_config_flow.py Co-authored-by: Teemu R. <tpr@iki.fi> * fix tests * define specific exceptions * fix styling * fix tests * use proper DeviceException * Revert "use proper DeviceException" This reverts commit 0bd16135387cd6d9e563cd62ac147d0a25c577f3. * use appropriate side-effect * remove unused returns * Update homeassistant/components/xiaomi_miio/const.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * remove unused returns Co-authored-by: Franck Nijhof <git@frenck.dev> Co-authored-by: Teemu R. <tpr@iki.fi> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>pull/57738/head
parent
c243dca58e
commit
e34aed743c
|
@ -35,6 +35,7 @@ from miio.gateway.gateway import GatewayException
|
|||
from homeassistant import config_entries, core
|
||||
from homeassistant.const import CONF_HOST, CONF_TOKEN
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
|
@ -65,6 +66,8 @@ from .const import (
|
|||
MODELS_PURIFIER_MIOT,
|
||||
MODELS_SWITCH,
|
||||
MODELS_VACUUM,
|
||||
AuthException,
|
||||
SetupException,
|
||||
)
|
||||
from .gateway import ConnectXiaomiGateway
|
||||
|
||||
|
@ -100,10 +103,9 @@ async def async_setup_entry(
|
|||
):
|
||||
"""Set up the Xiaomi Miio components from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
if entry.data[
|
||||
CONF_FLOW_TYPE
|
||||
] == CONF_GATEWAY and not await async_setup_gateway_entry(hass, entry):
|
||||
return False
|
||||
if entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY:
|
||||
await async_setup_gateway_entry(hass, entry)
|
||||
return True
|
||||
|
||||
return bool(
|
||||
entry.data[CONF_FLOW_TYPE] != CONF_DEVICE
|
||||
|
@ -362,8 +364,12 @@ async def async_setup_gateway_entry(
|
|||
|
||||
# Connect to gateway
|
||||
gateway = ConnectXiaomiGateway(hass, entry)
|
||||
if not await gateway.async_connect_gateway(host, token):
|
||||
return False
|
||||
try:
|
||||
await gateway.async_connect_gateway(host, token)
|
||||
except AuthException as error:
|
||||
raise ConfigEntryAuthFailed() from error
|
||||
except SetupException as error:
|
||||
raise ConfigEntryNotReady() from error
|
||||
gateway_info = gateway.gateway_info
|
||||
|
||||
gateway_model = f"{gateway_info.model}-{gateway_info.hardware_version}"
|
||||
|
@ -416,8 +422,6 @@ async def async_setup_gateway_entry(
|
|||
hass.config_entries.async_forward_entry_setup(entry, platform)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_device_entry(
|
||||
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
|
||||
|
|
|
@ -3,6 +3,7 @@ import logging
|
|||
from re import search
|
||||
|
||||
from micloud import MiCloud
|
||||
from micloud.micloudexception import MiCloudAccessDenied
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
|
@ -28,6 +29,8 @@ from .const import (
|
|||
MODELS_ALL_DEVICES,
|
||||
MODELS_GATEWAY,
|
||||
SERVER_COUNTRY_CODES,
|
||||
AuthException,
|
||||
SetupException,
|
||||
)
|
||||
from .device import ConnectXiaomiDevice
|
||||
|
||||
|
@ -230,8 +233,13 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
)
|
||||
|
||||
miio_cloud = MiCloud(cloud_username, cloud_password)
|
||||
if not await self.hass.async_add_executor_job(miio_cloud.login):
|
||||
try:
|
||||
if not await self.hass.async_add_executor_job(miio_cloud.login):
|
||||
errors["base"] = "cloud_login_error"
|
||||
except MiCloudAccessDenied:
|
||||
errors["base"] = "cloud_login_error"
|
||||
|
||||
if errors:
|
||||
return self.async_show_form(
|
||||
step_id="cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors
|
||||
)
|
||||
|
@ -320,14 +328,24 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
|
||||
# Try to connect to a Xiaomi Device.
|
||||
connect_device_class = ConnectXiaomiDevice(self.hass)
|
||||
await connect_device_class.async_connect_device(self.host, self.token)
|
||||
try:
|
||||
await connect_device_class.async_connect_device(self.host, self.token)
|
||||
except AuthException:
|
||||
if self.model is None:
|
||||
errors["base"] = "wrong_token"
|
||||
except SetupException:
|
||||
if self.model is None:
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
device_info = connect_device_class.device_info
|
||||
|
||||
if self.model is None and device_info is not None:
|
||||
self.model = device_info.model
|
||||
|
||||
if self.model is None:
|
||||
if self.model is None and not errors:
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
if errors:
|
||||
return self.async_show_form(
|
||||
step_id="connect", data_schema=DEVICE_MODEL_CONFIG, errors=errors
|
||||
)
|
||||
|
|
|
@ -37,6 +37,16 @@ SUCCESS = ["ok"]
|
|||
SERVER_COUNTRY_CODES = ["cn", "de", "i2", "ru", "sg", "us"]
|
||||
DEFAULT_CLOUD_COUNTRY = "cn"
|
||||
|
||||
|
||||
# Exceptions
|
||||
class AuthException(Exception):
|
||||
"""Exception indicating an authentication error."""
|
||||
|
||||
|
||||
class SetupException(Exception):
|
||||
"""Exception indicating a failure during setup."""
|
||||
|
||||
|
||||
# Fan Models
|
||||
MODEL_AIRPURIFIER_2H = "zhimi.airpurifier.mc2"
|
||||
MODEL_AIRPURIFIER_2S = "zhimi.airpurifier.mc1"
|
||||
|
|
|
@ -7,12 +7,11 @@ import logging
|
|||
from construct.core import ChecksumError
|
||||
from miio import Device, DeviceException
|
||||
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import CONF_MAC, CONF_MODEL, DOMAIN
|
||||
from .const import CONF_MAC, CONF_MODEL, DOMAIN, AuthException, SetupException
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -48,14 +47,11 @@ class ConnectXiaomiDevice:
|
|||
)
|
||||
except DeviceException as error:
|
||||
if isinstance(error.__cause__, ChecksumError):
|
||||
raise ConfigEntryAuthFailed(error) from error
|
||||
raise AuthException(error) from error
|
||||
|
||||
_LOGGER.error(
|
||||
"DeviceException during setup of xiaomi device with host %s: %s",
|
||||
host,
|
||||
error,
|
||||
)
|
||||
return False
|
||||
raise SetupException(
|
||||
f"DeviceException during setup of xiaomi device with host {host}"
|
||||
) from error
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s %s %s detected",
|
||||
|
@ -63,7 +59,6 @@ class ConnectXiaomiDevice:
|
|||
self._device_info.firmware_version,
|
||||
self._device_info.hardware_version,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
class XiaomiMiioEntity(Entity):
|
||||
|
|
|
@ -3,10 +3,10 @@ import logging
|
|||
|
||||
from construct.core import ChecksumError
|
||||
from micloud import MiCloud
|
||||
from micloud.micloudexception import MiCloudAccessDenied
|
||||
from miio import DeviceException, gateway
|
||||
from miio.gateway.gateway import GATEWAY_MODEL_EU
|
||||
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
|
@ -17,6 +17,8 @@ from .const import (
|
|||
CONF_CLOUD_SUBDEVICES,
|
||||
CONF_CLOUD_USERNAME,
|
||||
DOMAIN,
|
||||
AuthException,
|
||||
SetupException,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -59,8 +61,7 @@ class ConnectXiaomiGateway:
|
|||
self._cloud_password = self._config_entry.data.get(CONF_CLOUD_PASSWORD)
|
||||
self._cloud_country = self._config_entry.data.get(CONF_CLOUD_COUNTRY)
|
||||
|
||||
if not await self._hass.async_add_executor_job(self.connect_gateway):
|
||||
return False
|
||||
await self._hass.async_add_executor_job(self.connect_gateway)
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s %s %s detected",
|
||||
|
@ -68,7 +69,6 @@ class ConnectXiaomiGateway:
|
|||
self._gateway_info.firmware_version,
|
||||
self._gateway_info.hardware_version,
|
||||
)
|
||||
return True
|
||||
|
||||
def connect_gateway(self):
|
||||
"""Connect the gateway in a way that can called by async_add_executor_job."""
|
||||
|
@ -78,14 +78,11 @@ class ConnectXiaomiGateway:
|
|||
self._gateway_info = self._gateway_device.info()
|
||||
except DeviceException as error:
|
||||
if isinstance(error.__cause__, ChecksumError):
|
||||
raise ConfigEntryAuthFailed(error) from error
|
||||
raise AuthException(error) from error
|
||||
|
||||
_LOGGER.error(
|
||||
"DeviceException during setup of xiaomi gateway with host %s: %s",
|
||||
self._host,
|
||||
error,
|
||||
)
|
||||
return False
|
||||
raise SetupException(
|
||||
"DeviceException during setup of xiaomi gateway with host {self._host}"
|
||||
) from error
|
||||
|
||||
# get the connected sub devices
|
||||
use_cloud = self._use_cloud or self._gateway_info.model == GATEWAY_MODEL_EU
|
||||
|
@ -109,27 +106,27 @@ class ConnectXiaomiGateway:
|
|||
or self._cloud_password is None
|
||||
or self._cloud_country is None
|
||||
):
|
||||
raise ConfigEntryAuthFailed(
|
||||
raise AuthException(
|
||||
"Missing cloud credentials in Xiaomi Miio configuration"
|
||||
)
|
||||
|
||||
try:
|
||||
miio_cloud = MiCloud(self._cloud_username, self._cloud_password)
|
||||
if not miio_cloud.login():
|
||||
raise ConfigEntryAuthFailed(
|
||||
"Could not login to Xiaomi Miio Cloud, check the credentials"
|
||||
raise SetupException(
|
||||
"Failed to login to Xiaomi Miio Cloud during setup of Xiaomi"
|
||||
" gateway with host {self._host}",
|
||||
)
|
||||
devices_raw = miio_cloud.get_devices(self._cloud_country)
|
||||
self._gateway_device.get_devices_from_dict(devices_raw)
|
||||
except MiCloudAccessDenied as error:
|
||||
raise AuthException(
|
||||
"Could not login to Xiaomi Miio Cloud, check the credentials"
|
||||
) from error
|
||||
except DeviceException as error:
|
||||
_LOGGER.error(
|
||||
"DeviceException during setup of xiaomi gateway with host %s: %s",
|
||||
self._host,
|
||||
error,
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
raise SetupException(
|
||||
f"DeviceException during setup of xiaomi gateway with host {self._host}"
|
||||
) from error
|
||||
|
||||
|
||||
class XiaomiGatewayDevice(CoordinatorEntity, Entity):
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Xiaomi Miio",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
|
||||
"requirements": ["construct==2.10.56", "micloud==0.3", "python-miio==0.5.8"],
|
||||
"requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.8"],
|
||||
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"],
|
||||
"zeroconf": ["_miio._udp.local."],
|
||||
"iot_class": "local_polling"
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"wrong_token": "Checksum error, wrong token",
|
||||
"unknown_device": "The device model is not known, not able to setup the device using config flow.",
|
||||
"cloud_no_devices": "No devices found in this Xiaomi Miio cloud account.",
|
||||
"cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country",
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"wrong_token": "Checksum error, wrong token",
|
||||
"cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country",
|
||||
"cloud_login_error": "Could not login to Xiaomi Miio Cloud, check the credentials.",
|
||||
"cloud_no_devices": "No devices found in this Xiaomi Miio cloud account.",
|
||||
|
|
|
@ -993,7 +993,7 @@ meteofrance-api==1.0.2
|
|||
mficlient==0.3.0
|
||||
|
||||
# homeassistant.components.xiaomi_miio
|
||||
micloud==0.3
|
||||
micloud==0.4
|
||||
|
||||
# homeassistant.components.miflora
|
||||
miflora==0.7.0
|
||||
|
|
|
@ -588,7 +588,7 @@ meteofrance-api==1.0.2
|
|||
mficlient==0.3.0
|
||||
|
||||
# homeassistant.components.xiaomi_miio
|
||||
micloud==0.3
|
||||
micloud==0.4
|
||||
|
||||
# homeassistant.components.mill
|
||||
millheater==0.6.2
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""Test the Xiaomi Miio config flow."""
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from construct.core import ChecksumError
|
||||
from micloud.micloudexception import MiCloudAccessDenied
|
||||
from miio import DeviceException
|
||||
import pytest
|
||||
|
||||
|
@ -300,6 +302,23 @@ async def test_config_flow_gateway_cloud_login_error(hass):
|
|||
assert result["step_id"] == "cloud"
|
||||
assert result["errors"] == {"base": "cloud_login_error"}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.xiaomi_miio.config_flow.MiCloud.login",
|
||||
side_effect=MiCloudAccessDenied({}),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
|
||||
const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
|
||||
const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "cloud"
|
||||
assert result["errors"] == {"base": "cloud_login_error"}
|
||||
|
||||
|
||||
async def test_config_flow_gateway_cloud_no_devices(hass):
|
||||
"""Test a failed config flow using cloud with no devices."""
|
||||
|
@ -540,8 +559,8 @@ async def test_import_flow_success(hass):
|
|||
}
|
||||
|
||||
|
||||
async def test_config_flow_step_device_manual_model_succes(hass):
|
||||
"""Test config flow, device connection error, manual model."""
|
||||
async def test_config_flow_step_device_manual_model_error(hass):
|
||||
"""Test config flow, device connection error, model None."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
@ -561,7 +580,7 @@ async def test_config_flow_step_device_manual_model_succes(hass):
|
|||
|
||||
with patch(
|
||||
"homeassistant.components.xiaomi_miio.device.Device.info",
|
||||
side_effect=DeviceException({}),
|
||||
return_value=get_mock_info(model=None),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
|
@ -572,6 +591,41 @@ async def test_config_flow_step_device_manual_model_succes(hass):
|
|||
assert result["step_id"] == "connect"
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_config_flow_step_device_manual_model_succes(hass):
|
||||
"""Test config flow, device connection error, manual model."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "cloud"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{const.CONF_MANUAL: True},
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "manual"
|
||||
assert result["errors"] == {}
|
||||
|
||||
error = DeviceException({})
|
||||
error.__cause__ = ChecksumError({})
|
||||
with patch(
|
||||
"homeassistant.components.xiaomi_miio.device.Device.info",
|
||||
side_effect=error,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN},
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "connect"
|
||||
assert result["errors"] == {"base": "wrong_token"}
|
||||
|
||||
overwrite_model = const.MODELS_VACUUM[0]
|
||||
|
||||
with patch(
|
||||
|
|
Loading…
Reference in New Issue