Add Xiaomi Miio EU gateway support (#47955)

* Add EU gateway support

* add options flow translations

* fix options flow

* fix missing import

* try to fix async_add_executor_job

* try to fix async_add_executor_job

* fix unload

* check for login succes

* fix not reloading

* use cloud option

* fix styling

* Return after if

Co-authored-by: Nathan Tilley <nathan@tilley.xyz>

* cleanup

* add options flow tests

* fix new tests

* fix typo in docstring

* add missing blank line

* Use async_on_unload

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Use async_on_unload

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Use async_setup_platforms

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Use async_unload_platforms

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/xiaomi_miio/__init__.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/xiaomi_miio/const.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* default use_cloud False

* add options flow checks

* fix styling

* fix issort

* add MiCloud check tests

* fix indent

* fix styling

* fix tests

* fix tests

* fix black

* re-write config flow

* add explicit return type

* update strings.json

* black formatting

* fix config flow

Tested the config flow and it is now fully working

* fix styling

* Fix current tests

* Add missing tests

* fix styling

* add re-auth flow

* fix styling

* fix reauth flow

* Add reauth flow test

* use ConfigEntryAuthFailed

* also trigger reauth @ login error

* fix styling

* remove unused import

* fix spelling

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Fix spelling

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* fix spelling

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* remove unessesary .keys()

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* combine async_add_executor_job calls

* remove async_step_model

* fix wrong indent

* fix gatway.py

* fix tests

Co-authored-by: Nathan Tilley <nathan@tilley.xyz>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
starkillerOG 2021-06-15 00:05:40 +02:00 committed by GitHub
parent 8705168fe6
commit 3a2d50fe23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 1050 additions and 198 deletions

View file

@ -51,6 +51,30 @@ async def async_setup_entry(
)
def get_platforms(config_entry):
"""Return the platforms belonging to a config_entry."""
model = config_entry.data[CONF_MODEL]
flow_type = config_entry.data[CONF_FLOW_TYPE]
if flow_type == CONF_GATEWAY:
return GATEWAY_PLATFORMS
if flow_type == CONF_DEVICE:
if model in MODELS_SWITCH:
return SWITCH_PLATFORMS
if model in MODELS_FAN:
return FAN_PLATFORMS
if model in MODELS_LIGHT:
return LIGHT_PLATFORMS
for vacuum_model in MODELS_VACUUM:
if model.startswith(vacuum_model):
return VACUUM_PLATFORMS
for air_monitor_model in MODELS_AIR_MONITOR:
if model.startswith(air_monitor_model):
return AIR_MONITOR_PLATFORMS
return []
async def async_setup_gateway_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
):
@ -64,8 +88,10 @@ async def async_setup_gateway_entry(
if entry.unique_id.endswith("-gateway"):
hass.config_entries.async_update_entry(entry, unique_id=entry.data["mac"])
entry.async_on_unload(entry.add_update_listener(update_listener))
# Connect to gateway
gateway = ConnectXiaomiGateway(hass)
gateway = ConnectXiaomiGateway(hass, entry)
if not await gateway.async_connect_gateway(host, token):
return False
gateway_info = gateway.gateway_info
@ -128,29 +154,36 @@ async def async_setup_device_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
):
"""Set up the Xiaomi Miio device component from a config entry."""
model = entry.data[CONF_MODEL]
# Identify platforms to setup
platforms = []
if model in MODELS_SWITCH:
platforms = SWITCH_PLATFORMS
elif model in MODELS_FAN:
platforms = FAN_PLATFORMS
elif model in MODELS_LIGHT:
platforms = LIGHT_PLATFORMS
for vacuum_model in MODELS_VACUUM:
if model.startswith(vacuum_model):
platforms = VACUUM_PLATFORMS
for air_monitor_model in MODELS_AIR_MONITOR:
if model.startswith(air_monitor_model):
platforms = AIR_MONITOR_PLATFORMS
platforms = get_platforms(entry)
if not platforms:
return False
for platform in platforms:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
)
entry.async_on_unload(entry.add_update_listener(update_listener))
hass.config_entries.async_setup_platforms(entry, platforms)
return True
async def async_unload_entry(
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
):
"""Unload a config entry."""
platforms = get_platforms(config_entry)
unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, platforms
)
if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok
async def update_listener(
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
):
"""Handle options update."""
await hass.config_entries.async_reload(config_entry.entry_id)

View file

@ -2,34 +2,98 @@
import logging
from re import search
from micloud import MiCloud
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
from homeassistant.core import callback
from homeassistant.helpers.device_registry import format_mac
from .const import (
CONF_CLOUD_COUNTRY,
CONF_CLOUD_PASSWORD,
CONF_CLOUD_SUBDEVICES,
CONF_CLOUD_USERNAME,
CONF_DEVICE,
CONF_FLOW_TYPE,
CONF_GATEWAY,
CONF_MAC,
CONF_MANUAL,
CONF_MODEL,
DEFAULT_CLOUD_COUNTRY,
DOMAIN,
MODELS_ALL,
MODELS_ALL_DEVICES,
MODELS_GATEWAY,
SERVER_COUNTRY_CODES,
)
from .device import ConnectXiaomiDevice
_LOGGER = logging.getLogger(__name__)
DEFAULT_GATEWAY_NAME = "Xiaomi Gateway"
DEVICE_SETTINGS = {
vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)),
}
DEVICE_CONFIG = vol.Schema({vol.Required(CONF_HOST): str}).extend(DEVICE_SETTINGS)
DEVICE_MODEL_CONFIG = {vol.Optional(CONF_MODEL): vol.In(MODELS_ALL)}
DEVICE_MODEL_CONFIG = vol.Schema({vol.Required(CONF_MODEL): vol.In(MODELS_ALL)})
DEVICE_CLOUD_CONFIG = vol.Schema(
{
vol.Optional(CONF_CLOUD_USERNAME): str,
vol.Optional(CONF_CLOUD_PASSWORD): str,
vol.Optional(CONF_CLOUD_COUNTRY, default=DEFAULT_CLOUD_COUNTRY): vol.In(
SERVER_COUNTRY_CODES
),
vol.Optional(CONF_MANUAL, default=False): bool,
}
)
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Options for the component."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Init object."""
self.config_entry = config_entry
async def async_step_init(self, user_input=None):
"""Manage the options."""
errors = {}
if user_input is not None:
use_cloud = user_input.get(CONF_CLOUD_SUBDEVICES, False)
cloud_username = self.config_entry.data.get(CONF_CLOUD_USERNAME)
cloud_password = self.config_entry.data.get(CONF_CLOUD_PASSWORD)
cloud_country = self.config_entry.data.get(CONF_CLOUD_COUNTRY)
if use_cloud and (
not cloud_username or not cloud_password or not cloud_country
):
errors["base"] = "cloud_credentials_incomplete"
# trigger re-auth flow
self.hass.async_create_task(
self.hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_REAUTH},
data=self.config_entry.data,
)
)
if not errors:
return self.async_create_entry(title="", data=user_input)
settings_schema = vol.Schema(
{
vol.Optional(
CONF_CLOUD_SUBDEVICES,
default=self.config_entry.options.get(CONF_CLOUD_SUBDEVICES, False),
): bool
}
)
return self.async_show_form(
step_id="init", data_schema=settings_schema, errors=errors
)
class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@ -41,16 +105,51 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Initialize."""
self.host = None
self.mac = None
self.token = None
self.model = None
self.name = None
self.cloud_username = None
self.cloud_password = None
self.cloud_country = None
self.cloud_devices = {}
@staticmethod
@callback
def async_get_options_flow(config_entry) -> OptionsFlowHandler:
"""Get the options flow."""
return OptionsFlowHandler(config_entry)
async def async_step_reauth(self, user_input=None):
"""Perform reauth upon an authentication error or missing cloud credentials."""
self.host = user_input[CONF_HOST]
self.token = user_input[CONF_TOKEN]
self.mac = user_input[CONF_MAC]
self.model = user_input.get(CONF_MODEL)
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(self, user_input=None):
"""Dialog that informs the user that reauth is required."""
if user_input is not None:
return await self.async_step_cloud()
return self.async_show_form(
step_id="reauth_confirm", data_schema=vol.Schema({})
)
async def async_step_import(self, conf: dict):
"""Import a configuration from config.yaml."""
host = conf[CONF_HOST]
self.context.update({"title_placeholders": {"name": f"YAML import {host}"}})
return await self.async_step_device(user_input=conf)
self.host = conf[CONF_HOST]
self.token = conf[CONF_TOKEN]
self.name = conf.get(CONF_NAME)
self.model = conf.get(CONF_MODEL)
self.context.update(
{"title_placeholders": {"name": f"YAML import {self.host}"}}
)
return await self.async_step_connect()
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
return await self.async_step_device()
return await self.async_step_cloud()
async def async_step_zeroconf(self, discovery_info):
"""Handle zeroconf discovery."""
@ -79,7 +178,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
{"title_placeholders": {"name": f"Gateway {self.host}"}}
)
return await self.async_step_device()
return await self.async_step_cloud()
for device_model in MODELS_ALL_DEVICES:
if name.startswith(device_model.replace(".", "-")):
@ -91,7 +190,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
{"title_placeholders": {"name": f"{device_model} {self.host}"}}
)
return await self.async_step_device()
return await self.async_step_cloud()
# Discovered device is not yet supported
_LOGGER.debug(
@ -101,76 +200,190 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
)
return self.async_abort(reason="not_xiaomi_miio")
async def async_step_device(self, user_input=None):
"""Handle a flow initialized by the user to configure a xiaomi miio device."""
def extract_cloud_info(self, cloud_device_info):
"""Extract the cloud info."""
if self.host is None:
self.host = cloud_device_info["localip"]
if self.mac is None:
self.mac = format_mac(cloud_device_info["mac"])
if self.model is None:
self.model = cloud_device_info["model"]
if self.name is None:
self.name = cloud_device_info["name"]
self.token = cloud_device_info["token"]
async def async_step_cloud(self, user_input=None):
"""Configure a xiaomi miio device through the Miio Cloud."""
errors = {}
if user_input is not None:
token = user_input[CONF_TOKEN]
model = user_input.get(CONF_MODEL)
if user_input[CONF_MANUAL]:
return await self.async_step_manual()
cloud_username = user_input.get(CONF_CLOUD_USERNAME)
cloud_password = user_input.get(CONF_CLOUD_PASSWORD)
cloud_country = user_input.get(CONF_CLOUD_COUNTRY)
if not cloud_username or not cloud_password or not cloud_country:
errors["base"] = "cloud_credentials_incomplete"
return self.async_show_form(
step_id="cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors
)
miio_cloud = MiCloud(cloud_username, cloud_password)
if not await self.hass.async_add_executor_job(miio_cloud.login):
errors["base"] = "cloud_login_error"
return self.async_show_form(
step_id="cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors
)
devices_raw = await self.hass.async_add_executor_job(
miio_cloud.get_devices, cloud_country
)
if not devices_raw:
errors["base"] = "cloud_no_devices"
return self.async_show_form(
step_id="cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors
)
self.cloud_devices = {}
for device in devices_raw:
parent_id = device.get("parent_id")
if not parent_id:
name = device["name"]
model = device["model"]
list_name = f"{name} - {model}"
self.cloud_devices[list_name] = device
self.cloud_username = cloud_username
self.cloud_password = cloud_password
self.cloud_country = cloud_country
if self.host is not None:
for device in self.cloud_devices.values():
cloud_host = device.get("localip")
if cloud_host == self.host:
self.extract_cloud_info(device)
return await self.async_step_connect()
if len(self.cloud_devices) == 1:
self.extract_cloud_info(list(self.cloud_devices.values())[0])
return await self.async_step_connect()
return await self.async_step_select()
return self.async_show_form(
step_id="cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors
)
async def async_step_select(self, user_input=None):
"""Handle multiple cloud devices found."""
errors = {}
if user_input is not None:
cloud_device = self.cloud_devices[user_input["select_device"]]
self.extract_cloud_info(cloud_device)
return await self.async_step_connect()
select_schema = vol.Schema(
{vol.Required("select_device"): vol.In(list(self.cloud_devices))}
)
return self.async_show_form(
step_id="select", data_schema=select_schema, errors=errors
)
async def async_step_manual(self, user_input=None):
"""Configure a xiaomi miio device Manually."""
errors = {}
if user_input is not None:
self.token = user_input[CONF_TOKEN]
if user_input.get(CONF_HOST):
self.host = user_input[CONF_HOST]
# Try to connect to a Xiaomi Device.
connect_device_class = ConnectXiaomiDevice(self.hass)
await connect_device_class.async_connect_device(self.host, token)
device_info = connect_device_class.device_info
if model is None and device_info is not None:
model = device_info.model
if model is not None:
if self.mac is None and device_info is not None:
self.mac = format_mac(device_info.mac_address)
# Setup Gateways
for gateway_model in MODELS_GATEWAY:
if model.startswith(gateway_model):
unique_id = self.mac
await self.async_set_unique_id(
unique_id, raise_on_progress=False
)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=DEFAULT_GATEWAY_NAME,
data={
CONF_FLOW_TYPE: CONF_GATEWAY,
CONF_HOST: self.host,
CONF_TOKEN: token,
CONF_MODEL: model,
CONF_MAC: self.mac,
},
)
# Setup all other Miio Devices
name = user_input.get(CONF_NAME, model)
for device_model in MODELS_ALL_DEVICES:
if model.startswith(device_model):
unique_id = self.mac
await self.async_set_unique_id(
unique_id, raise_on_progress=False
)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=name,
data={
CONF_FLOW_TYPE: CONF_DEVICE,
CONF_HOST: self.host,
CONF_TOKEN: token,
CONF_MODEL: model,
CONF_MAC: self.mac,
},
)
errors["base"] = "unknown_device"
else:
errors["base"] = "cannot_connect"
return await self.async_step_connect()
if self.host:
schema = vol.Schema(DEVICE_SETTINGS)
else:
schema = DEVICE_CONFIG
if errors:
schema = schema.extend(DEVICE_MODEL_CONFIG)
return self.async_show_form(step_id="manual", data_schema=schema, errors=errors)
return self.async_show_form(step_id="device", data_schema=schema, errors=errors)
async def async_step_connect(self, user_input=None):
"""Connect to a xiaomi miio device."""
errors = {}
if self.host is None or self.token is None:
return self.async_abort(reason="incomplete_info")
if user_input is not None:
self.model = user_input[CONF_MODEL]
# Try to connect to a Xiaomi Device.
connect_device_class = ConnectXiaomiDevice(self.hass)
await connect_device_class.async_connect_device(self.host, self.token)
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:
errors["base"] = "cannot_connect"
return self.async_show_form(
step_id="connect", data_schema=DEVICE_MODEL_CONFIG, errors=errors
)
if self.mac is None and device_info is not None:
self.mac = format_mac(device_info.mac_address)
unique_id = self.mac
existing_entry = await self.async_set_unique_id(
unique_id, raise_on_progress=False
)
if existing_entry:
data = existing_entry.data.copy()
data[CONF_HOST] = self.host
data[CONF_TOKEN] = self.token
if (
self.cloud_username is not None
and self.cloud_password is not None
and self.cloud_country is not None
):
data[CONF_CLOUD_USERNAME] = self.cloud_username
data[CONF_CLOUD_PASSWORD] = self.cloud_password
data[CONF_CLOUD_COUNTRY] = self.cloud_country
self.hass.config_entries.async_update_entry(existing_entry, data=data)
await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.async_abort(reason="reauth_successful")
if self.name is None:
self.name = self.model
flow_type = None
for gateway_model in MODELS_GATEWAY:
if self.model.startswith(gateway_model):
flow_type = CONF_GATEWAY
if flow_type is None:
for device_model in MODELS_ALL_DEVICES:
if self.model.startswith(device_model):
flow_type = CONF_DEVICE
if flow_type is not None:
return self.async_create_entry(
title=self.name,
data={
CONF_FLOW_TYPE: flow_type,
CONF_HOST: self.host,
CONF_TOKEN: self.token,
CONF_MODEL: self.model,
CONF_MAC: self.mac,
CONF_CLOUD_USERNAME: self.cloud_username,
CONF_CLOUD_PASSWORD: self.cloud_password,
CONF_CLOUD_COUNTRY: self.cloud_country,
},
)
errors["base"] = "unknown_device"
return self.async_show_form(
step_id="connect", data_schema=DEVICE_MODEL_CONFIG, errors=errors
)

View file

@ -1,16 +1,28 @@
"""Constants for the Xiaomi Miio component."""
DOMAIN = "xiaomi_miio"
# Config flow
CONF_FLOW_TYPE = "config_flow_device"
CONF_GATEWAY = "gateway"
CONF_DEVICE = "device"
CONF_MODEL = "model"
CONF_MAC = "mac"
CONF_CLOUD_USERNAME = "cloud_username"
CONF_CLOUD_PASSWORD = "cloud_password"
CONF_CLOUD_COUNTRY = "cloud_country"
CONF_MANUAL = "manual"
# Options flow
CONF_CLOUD_SUBDEVICES = "cloud_subdevices"
KEY_COORDINATOR = "coordinator"
ATTR_AVAILABLE = "available"
# Cloud
SERVER_COUNTRY_CODES = ["cn", "de", "i2", "ru", "sg", "us"]
DEFAULT_CLOUD_COUNTRY = "cn"
# Fan Models
MODEL_AIRPURIFIER_V1 = "zhimi.airpurifier.v1"
MODEL_AIRPURIFIER_V2 = "zhimi.airpurifier.v2"

View file

@ -1,12 +1,22 @@
"""Code to handle a Xiaomi Gateway."""
import logging
from micloud import MiCloud
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
from .const import ATTR_AVAILABLE, DOMAIN
from .const import (
ATTR_AVAILABLE,
CONF_CLOUD_COUNTRY,
CONF_CLOUD_PASSWORD,
CONF_CLOUD_SUBDEVICES,
CONF_CLOUD_USERNAME,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
@ -14,11 +24,18 @@ _LOGGER = logging.getLogger(__name__)
class ConnectXiaomiGateway:
"""Class to async connect to a Xiaomi Gateway."""
def __init__(self, hass):
def __init__(self, hass, config_entry):
"""Initialize the entity."""
self._hass = hass
self._config_entry = config_entry
self._gateway_device = None
self._gateway_info = None
self._use_cloud = None
self._cloud_username = None
self._cloud_password = None
self._cloud_country = None
self._host = None
self._token = None
@property
def gateway_device(self):
@ -33,21 +50,17 @@ class ConnectXiaomiGateway:
async def async_connect_gateway(self, host, token):
"""Connect to the Xiaomi Gateway."""
_LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
try:
self._gateway_device = gateway.Gateway(host, token)
# get the gateway info
self._gateway_info = await self._hass.async_add_executor_job(
self._gateway_device.info
)
# get the connected sub devices
await self._hass.async_add_executor_job(
self._gateway_device.discover_devices
)
except DeviceException:
_LOGGER.error(
"DeviceException during setup of xiaomi gateway with host %s", host
)
self._host = host
self._token = token
self._use_cloud = self._config_entry.options.get(CONF_CLOUD_SUBDEVICES, False)
self._cloud_username = self._config_entry.data.get(CONF_CLOUD_USERNAME)
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
_LOGGER.debug(
"%s %s %s detected",
self._gateway_info.model,
@ -56,6 +69,45 @@ class ConnectXiaomiGateway:
)
return True
def connect_gateway(self):
"""Connect the gateway in a way that can called by async_add_executor_job."""
try:
self._gateway_device = gateway.Gateway(self._host, self._token)
# get the gateway info
self._gateway_device.info()
# get the connected sub devices
if self._use_cloud or self._gateway_info.model == GATEWAY_MODEL_EU:
if (
self._cloud_username is None
or self._cloud_password is None
or self._cloud_country is None
):
raise ConfigEntryAuthFailed(
"Missing cloud credentials in Xiaomi Miio configuration"
)
# use miio-cloud
miio_cloud = MiCloud(self._cloud_username, self._cloud_password)
if not miio_cloud.login():
raise ConfigEntryAuthFailed(
"Could not login to Xioami Miio Cloud, check the credentials"
)
devices_raw = miio_cloud.get_devices(self._cloud_country)
self._gateway_device.get_devices_from_dict(devices_raw)
else:
# use local query (not supported by all gateway types)
self._gateway_device.discover_devices()
except DeviceException:
_LOGGER.error(
"DeviceException during setup of xiaomi gateway with host %s",
self._host,
)
return False
return True
class XiaomiGatewayDevice(CoordinatorEntity, Entity):
"""Representation of a base Xiaomi Gateway Device."""

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", "python-miio==0.5.6"],
"requirements": ["construct==2.10.56", "micloud==0.3", "python-miio==0.5.6"],
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG"],
"zeroconf": ["_miio._udp.local."],
"iot_class": "local_polling"

View file

@ -1,23 +1,70 @@
{
"config": {
"abort": {
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"incomplete_info": "Incomplete information to setup device, no host or token supplied.",
"not_xiaomi_miio": "Device is not (yet) supported by Xiaomi Miio."
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown_device": "The device model is not known, not able to setup the device using config flow."
"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",
"cloud_login_error": "Could not login to Xioami Miio Cloud, check the credentials."
},
"flow_title": "{name}",
"step": {
"device": {
"reauth_confirm": {
"description": "The Xiaomi Miio integration needs to re-authenticate your account in order to update the tokens or add missing cloud credentials.",
"title": "[%key:common::config_flow::title::reauth%]"
},
"cloud": {
"data": {
"cloud_username": "Cloud username",
"cloud_password": "Cloud password",
"cloud_country": "Cloud server country",
"manual": "Configure manually (not recommended)"
},
"description": "Log in to the Xiaomi Miio cloud, see https://www.openhab.org/addons/bindings/miio/#country-servers for the cloud server to use.",
"title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway"
},
"select": {
"data": {
"select_device": "Miio device"
},
"description": "Select the Xiaomi Miio device to setup.",
"title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway"
},
"manual": {
"data": {
"host": "[%key:common::config_flow::data::ip%]",
"model": "Device model (Optional)",
"token": "[%key:common::config_flow::data::api_token%]"
},
"description": "You will need the 32 character [%key:common::config_flow::data::api_token%], see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this [%key:common::config_flow::data::api_token%] is different from the key used by the Xiaomi Aqara integration.",
"title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway"
},
"connect": {
"data": {
"model": "Device model"
},
"description": "Manually select the device model from the supported models.",
"title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway"
}
}
},
"options": {
"error": {
"cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country"
},
"step": {
"init": {
"title": "Xiaomi Miio",
"description": "Specify optional settings",
"data": {
"cloud_subdevices": "Use cloud to get connected subdevices"
}
}
}
}

View file

@ -1,42 +1,71 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress"
"config": {
"abort": {
"reauth_successful": "Re-authentication was successful",
"already_configured": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress",
"incomplete_info": "Incomplete information to setup device, no host or token supplied.",
"not_xiaomi_miio": "Device is not (yet) supported by Xiaomi Miio."
},
"error": {
"cannot_connect": "Failed to connect",
"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",
"cloud_login_error": "Could not login to Xioami Miio Cloud, check the credentials."
},
"flow_title": "{name}",
"step": {
"reauth_confirm": {
"description": "The Xiaomi Miio integration needs to re-authenticate your acount in order to update the tokens or add missing cloud credentials.",
"title": "Reauthenticate Integration"
},
"cloud": {
"data": {
"cloud_username": "Cloud username",
"cloud_password": "Cloud password",
"cloud_country": "Cloud server country",
"manual": "Configure manually (not recommended)"
},
"error": {
"cannot_connect": "Failed to connect",
"no_device_selected": "No device selected, please select one device.",
"unknown_device": "The device model is not known, not able to setup the device using config flow."
"description": "Log in to the Xiaomi Miio cloud, see https://www.openhab.org/addons/bindings/miio/#country-servers for the cloud server to use.",
"title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway"
},
"select": {
"data": {
"select_device": "Miio device"
},
"flow_title": "{name}",
"step": {
"device": {
"data": {
"host": "IP Address",
"model": "Device model (Optional)",
"name": "Name of the device",
"token": "API Token"
},
"description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.",
"title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway"
},
"gateway": {
"data": {
"host": "IP Address",
"name": "Name of the Gateway",
"token": "API Token"
},
"description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.",
"title": "Connect to a Xiaomi Gateway"
},
"user": {
"data": {
"gateway": "Connect to a Xiaomi Gateway"
},
"description": "Select to which device you want to connect.",
"title": "Xiaomi Miio"
}
}
"description": "Select the Xiaomi Miio device to setup.",
"title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway"
},
"manual": {
"data": {
"host": "IP Address",
"token": "API Token"
},
"description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.",
"title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway"
},
"connect": {
"data": {
"model": "Device model"
},
"description": "Manually select the device model from the supported models.",
"title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway"
}
}
}
},
"options": {
"error": {
"cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country"
},
"step": {
"init": {
"title": "Xiaomi Miio",
"description": "Specify optional settings",
"data": {
"cloud_subdevices": "Use cloud to get connected subdevices"
}
}
}
}
}

View file

@ -956,6 +956,9 @@ meteofrance-api==1.0.2
# homeassistant.components.mfi
mficlient==0.3.0
# homeassistant.components.xiaomi_miio
micloud==0.3
# homeassistant.components.miflora
miflora==0.7.0

View file

@ -528,6 +528,9 @@ meteofrance-api==1.0.2
# homeassistant.components.mfi
mficlient==0.3.0
# homeassistant.components.xiaomi_miio
micloud==0.3
# homeassistant.components.mill
millheater==0.4.1

View file

@ -2,27 +2,86 @@
from unittest.mock import Mock, patch
from miio import DeviceException
import pytest
from homeassistant import config_entries
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.xiaomi_miio import const
from homeassistant.components.xiaomi_miio.config_flow import DEFAULT_GATEWAY_NAME
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
from tests.common import MockConfigEntry
ZEROCONF_NAME = "name"
ZEROCONF_PROP = "properties"
ZEROCONF_MAC = "mac"
TEST_HOST = "1.2.3.4"
TEST_HOST2 = "5.6.7.8"
TEST_CLOUD_USER = "username"
TEST_CLOUD_PASS = "password"
TEST_CLOUD_COUNTRY = "cn"
TEST_TOKEN = "12345678901234567890123456789012"
TEST_NAME = "Test_Gateway"
TEST_NAME2 = "Test_Gateway_2"
TEST_MODEL = const.MODELS_GATEWAY[0]
TEST_MAC = "ab:cd:ef:gh:ij:kl"
TEST_MAC2 = "mn:op:qr:st:uv:wx"
TEST_MAC_DEVICE = "abcdefghijkl"
TEST_MAC_DEVICE2 = "mnopqrstuvwx"
TEST_GATEWAY_ID = TEST_MAC
TEST_HARDWARE_VERSION = "AB123"
TEST_FIRMWARE_VERSION = "1.2.3_456"
TEST_ZEROCONF_NAME = "lumi-gateway-v3_miio12345678._miio._udp.local."
TEST_SUB_DEVICE_LIST = []
TEST_CLOUD_DEVICES_1 = [
{
"parent_id": None,
"name": TEST_NAME,
"model": TEST_MODEL,
"localip": TEST_HOST,
"mac": TEST_MAC_DEVICE,
"token": TEST_TOKEN,
}
]
TEST_CLOUD_DEVICES_2 = [
{
"parent_id": None,
"name": TEST_NAME,
"model": TEST_MODEL,
"localip": TEST_HOST,
"mac": TEST_MAC_DEVICE,
"token": TEST_TOKEN,
},
{
"parent_id": None,
"name": TEST_NAME2,
"model": TEST_MODEL,
"localip": TEST_HOST2,
"mac": TEST_MAC_DEVICE2,
"token": TEST_TOKEN,
},
]
@pytest.fixture(name="xiaomi_miio_connect", autouse=True)
def xiaomi_miio_connect_fixture():
"""Mock denonavr connection and entry setup."""
mock_info = get_mock_info()
with patch(
"homeassistant.components.xiaomi_miio.device.Device.info",
return_value=mock_info,
), patch(
"homeassistant.components.xiaomi_miio.config_flow.MiCloud.login",
return_value=True,
), patch(
"homeassistant.components.xiaomi_miio.config_flow.MiCloud.get_devices",
return_value=TEST_CLOUD_DEVICES_1,
), patch(
"homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
), patch(
"homeassistant.components.xiaomi_miio.async_unload_entry", return_value=True
):
yield
def get_mock_info(
@ -48,7 +107,16 @@ async def test_config_flow_step_gateway_connect_error(hass):
)
assert result["type"] == "form"
assert result["step_id"] == "device"
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"] == {}
with patch(
@ -61,7 +129,7 @@ async def test_config_flow_step_gateway_connect_error(hass):
)
assert result["type"] == "form"
assert result["step_id"] == "device"
assert result["step_id"] == "connect"
assert result["errors"] == {"base": "cannot_connect"}
@ -72,26 +140,30 @@ async def test_config_flow_gateway_success(hass):
)
assert result["type"] == "form"
assert result["step_id"] == "device"
assert result["step_id"] == "cloud"
assert result["errors"] == {}
mock_info = get_mock_info()
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{const.CONF_MANUAL: True},
)
with patch(
"homeassistant.components.xiaomi_miio.device.Device.info",
return_value=mock_info,
), patch(
"homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
):
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"] == "manual"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN},
)
assert result["type"] == "create_entry"
assert result["title"] == DEFAULT_GATEWAY_NAME
assert result["title"] == TEST_MODEL
assert result["data"] == {
const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
const.CONF_CLOUD_USERNAME: None,
const.CONF_CLOUD_PASSWORD: None,
const.CONF_CLOUD_COUNTRY: None,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: TEST_MODEL,
@ -99,6 +171,202 @@ async def test_config_flow_gateway_success(hass):
}
async def test_config_flow_gateway_cloud_success(hass):
"""Test a successful config flow using cloud."""
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_CLOUD_USERNAME: TEST_CLOUD_USER,
const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
},
)
assert result["type"] == "create_entry"
assert result["title"] == TEST_NAME
assert result["data"] == {
const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: TEST_MODEL,
const.CONF_MAC: TEST_MAC,
}
async def test_config_flow_gateway_cloud_multiple_success(hass):
"""Test a successful config flow using cloud with multiple devices."""
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"] == {}
with patch(
"homeassistant.components.xiaomi_miio.config_flow.MiCloud.get_devices",
return_value=TEST_CLOUD_DEVICES_2,
):
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"] == "select"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"select_device": f"{TEST_NAME2} - {TEST_MODEL}"},
)
assert result["type"] == "create_entry"
assert result["title"] == TEST_NAME2
assert result["data"] == {
const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
CONF_HOST: TEST_HOST2,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: TEST_MODEL,
const.CONF_MAC: TEST_MAC2,
}
async def test_config_flow_gateway_cloud_incomplete(hass):
"""Test a failed config flow using incomplete cloud credentials."""
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_CLOUD_USERNAME: TEST_CLOUD_USER,
const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
},
)
assert result["type"] == "form"
assert result["step_id"] == "cloud"
assert result["errors"] == {"base": "cloud_credentials_incomplete"}
async def test_config_flow_gateway_cloud_login_error(hass):
"""Test a failed config flow using cloud login error."""
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"] == {}
with patch(
"homeassistant.components.xiaomi_miio.config_flow.MiCloud.login",
return_value=False,
):
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."""
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"] == {}
with patch(
"homeassistant.components.xiaomi_miio.config_flow.MiCloud.get_devices",
return_value=[],
):
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_no_devices"}
async def test_config_flow_gateway_cloud_missing_token(hass):
"""Test a failed config flow using cloud with a missing token."""
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"] == {}
cloud_device = [
{
"parent_id": None,
"name": TEST_NAME,
"model": TEST_MODEL,
"localip": TEST_HOST,
"mac": TEST_MAC_DEVICE,
"token": None,
}
]
with patch(
"homeassistant.components.xiaomi_miio.config_flow.MiCloud.get_devices",
return_value=cloud_device,
):
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"] == "abort"
assert result["reason"] == "incomplete_info"
async def test_zeroconf_gateway_success(hass):
"""Test a successful zeroconf discovery of a gateway."""
result = await hass.config_entries.flow.async_init(
@ -112,26 +380,25 @@ async def test_zeroconf_gateway_success(hass):
)
assert result["type"] == "form"
assert result["step_id"] == "device"
assert result["step_id"] == "cloud"
assert result["errors"] == {}
mock_info = get_mock_info()
with patch(
"homeassistant.components.xiaomi_miio.device.Device.info",
return_value=mock_info,
), patch(
"homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: TEST_TOKEN},
)
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"] == "create_entry"
assert result["title"] == DEFAULT_GATEWAY_NAME
assert result["title"] == TEST_NAME
assert result["data"] == {
const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: TEST_MODEL,
@ -184,7 +451,16 @@ async def test_config_flow_step_device_connect_error(hass):
)
assert result["type"] == "form"
assert result["step_id"] == "device"
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"] == {}
with patch(
@ -197,7 +473,7 @@ async def test_config_flow_step_device_connect_error(hass):
)
assert result["type"] == "form"
assert result["step_id"] == "device"
assert result["step_id"] == "connect"
assert result["errors"] == {"base": "cannot_connect"}
@ -208,7 +484,16 @@ async def test_config_flow_step_unknown_device(hass):
)
assert result["type"] == "form"
assert result["step_id"] == "device"
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"] == {}
mock_info = get_mock_info(model="UNKNOWN")
@ -223,7 +508,7 @@ async def test_config_flow_step_unknown_device(hass):
)
assert result["type"] == "form"
assert result["step_id"] == "device"
assert result["step_id"] == "connect"
assert result["errors"] == {"base": "unknown_device"}
@ -234,8 +519,6 @@ async def test_import_flow_success(hass):
with patch(
"homeassistant.components.xiaomi_miio.device.Device.info",
return_value=mock_info,
), patch(
"homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_init(
const.DOMAIN,
@ -247,6 +530,9 @@ async def test_import_flow_success(hass):
assert result["title"] == TEST_NAME
assert result["data"] == {
const.CONF_FLOW_TYPE: const.CONF_DEVICE,
const.CONF_CLOUD_USERNAME: None,
const.CONF_CLOUD_PASSWORD: None,
const.CONF_CLOUD_COUNTRY: None,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: const.MODELS_SWITCH[0],
@ -261,7 +547,16 @@ async def test_config_flow_step_device_manual_model_succes(hass):
)
assert result["type"] == "form"
assert result["step_id"] == "device"
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"] == {}
with patch(
@ -274,7 +569,7 @@ async def test_config_flow_step_device_manual_model_succes(hass):
)
assert result["type"] == "form"
assert result["step_id"] == "device"
assert result["step_id"] == "connect"
assert result["errors"] == {"base": "cannot_connect"}
overwrite_model = const.MODELS_VACUUM[0]
@ -282,18 +577,19 @@ async def test_config_flow_step_device_manual_model_succes(hass):
with patch(
"homeassistant.components.xiaomi_miio.device.Device.info",
side_effect=DeviceException({}),
), patch(
"homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: TEST_TOKEN, const.CONF_MODEL: overwrite_model},
{const.CONF_MODEL: overwrite_model},
)
assert result["type"] == "create_entry"
assert result["title"] == overwrite_model
assert result["data"] == {
const.CONF_FLOW_TYPE: const.CONF_DEVICE,
const.CONF_CLOUD_USERNAME: None,
const.CONF_CLOUD_PASSWORD: None,
const.CONF_CLOUD_COUNTRY: None,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: overwrite_model,
@ -308,7 +604,16 @@ async def config_flow_device_success(hass, model_to_test):
)
assert result["type"] == "form"
assert result["step_id"] == "device"
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"] == {}
mock_info = get_mock_info(model=model_to_test)
@ -316,8 +621,6 @@ async def config_flow_device_success(hass, model_to_test):
with patch(
"homeassistant.components.xiaomi_miio.device.Device.info",
return_value=mock_info,
), patch(
"homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
@ -328,6 +631,9 @@ async def config_flow_device_success(hass, model_to_test):
assert result["title"] == model_to_test
assert result["data"] == {
const.CONF_FLOW_TYPE: const.CONF_DEVICE,
const.CONF_CLOUD_USERNAME: None,
const.CONF_CLOUD_PASSWORD: None,
const.CONF_CLOUD_COUNTRY: None,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: model_to_test,
@ -348,7 +654,16 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test):
)
assert result["type"] == "form"
assert result["step_id"] == "device"
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"] == {}
mock_info = get_mock_info(model=model_to_test)
@ -356,8 +671,6 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test):
with patch(
"homeassistant.components.xiaomi_miio.device.Device.info",
return_value=mock_info,
), patch(
"homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
@ -368,6 +681,9 @@ async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test):
assert result["title"] == model_to_test
assert result["data"] == {
const.CONF_FLOW_TYPE: const.CONF_DEVICE,
const.CONF_CLOUD_USERNAME: None,
const.CONF_CLOUD_PASSWORD: None,
const.CONF_CLOUD_COUNTRY: None,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: model_to_test,
@ -399,3 +715,147 @@ async def test_zeroconf_vacuum_success(hass):
test_vacuum_model = const.MODELS_VACUUM[0]
test_zeroconf_name = const.MODELS_VACUUM[0].replace(".", "-")
await zeroconf_device_success(hass, test_zeroconf_name, test_vacuum_model)
async def test_options_flow(hass):
"""Test specifying non default settings using options flow."""
config_entry = MockConfigEntry(
domain=const.DOMAIN,
unique_id=TEST_GATEWAY_ID,
data={
const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: TEST_MODEL,
const.CONF_MAC: TEST_MAC,
},
title=TEST_NAME,
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
const.CONF_CLOUD_SUBDEVICES: True,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert config_entry.options == {
const.CONF_CLOUD_SUBDEVICES: True,
}
async def test_options_flow_incomplete(hass):
"""Test specifying incomplete settings using options flow."""
config_entry = MockConfigEntry(
domain=const.DOMAIN,
unique_id=TEST_GATEWAY_ID,
data={
const.CONF_CLOUD_USERNAME: None,
const.CONF_CLOUD_PASSWORD: None,
const.CONF_CLOUD_COUNTRY: None,
const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: TEST_MODEL,
const.CONF_MAC: TEST_MAC,
},
title=TEST_NAME,
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
const.CONF_CLOUD_SUBDEVICES: True,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
assert result["errors"] == {"base": "cloud_credentials_incomplete"}
async def test_reauth(hass):
"""Test a reauth flow."""
# await setup.async_setup_component(hass, "persistent_notification", {})
config_entry = MockConfigEntry(
domain=const.DOMAIN,
unique_id=TEST_GATEWAY_ID,
data={
const.CONF_CLOUD_USERNAME: None,
const.CONF_CLOUD_PASSWORD: None,
const.CONF_CLOUD_COUNTRY: None,
const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: TEST_MODEL,
const.CONF_MAC: TEST_MAC,
},
title=TEST_NAME,
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_init(
const.DOMAIN,
context={"source": config_entries.SOURCE_REAUTH},
data=config_entry.data,
)
assert result["type"] == "form"
assert result["step_id"] == "reauth_confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{},
)
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_CLOUD_USERNAME: TEST_CLOUD_USER,
const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
},
)
assert result["type"] == "abort"
assert result["reason"] == "reauth_successful"
config_data = config_entry.data.copy()
assert config_data == {
const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER,
const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS,
const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: TEST_MODEL,
const.CONF_MAC: TEST_MAC,
}