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
starkillerOG 2021-10-15 01:25:44 +02:00 committed by GitHub
parent c243dca58e
commit e34aed743c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 129 additions and 49 deletions

View File

@ -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

View File

@ -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
)

View File

@ -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"

View File

@ -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):

View File

@ -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):

View File

@ -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"

View File

@ -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",

View File

@ -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.",

View File

@ -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

View File

@ -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

View File

@ -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(