Add config flow to Xiaomi Miio switch (#46179)

pull/46590/head
starkillerOG 2021-02-15 20:11:27 +01:00 committed by GitHub
parent 68809e9f43
commit 2f9fda73f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 563 additions and 295 deletions

View File

@ -1085,6 +1085,7 @@ omit =
homeassistant/components/xiaomi_miio/__init__.py
homeassistant/components/xiaomi_miio/air_quality.py
homeassistant/components/xiaomi_miio/alarm_control_panel.py
homeassistant/components/xiaomi_miio/device.py
homeassistant/components/xiaomi_miio/device_tracker.py
homeassistant/components/xiaomi_miio/fan.py
homeassistant/components/xiaomi_miio/gateway.py

View File

@ -3,11 +3,18 @@ from homeassistant import config_entries, core
from homeassistant.const import CONF_HOST, CONF_TOKEN
from homeassistant.helpers import device_registry as dr
from .config_flow import CONF_FLOW_TYPE, CONF_GATEWAY
from .const import DOMAIN
from .const import (
CONF_DEVICE,
CONF_FLOW_TYPE,
CONF_GATEWAY,
CONF_MODEL,
DOMAIN,
MODELS_SWITCH,
)
from .gateway import ConnectXiaomiGateway
GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor", "light"]
SWITCH_PLATFORMS = ["switch"]
async def async_setup(hass: core.HomeAssistant, config: dict):
@ -19,10 +26,13 @@ async def async_setup_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
):
"""Set up the Xiaomi Miio components from a config entry."""
hass.data[DOMAIN] = {}
hass.data.setdefault(DOMAIN, {})
if entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY:
if not await async_setup_gateway_entry(hass, entry):
return False
if entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
if not await async_setup_device_entry(hass, entry):
return False
return True
@ -67,3 +77,23 @@ async def async_setup_gateway_entry(
)
return True
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
if model in MODELS_SWITCH:
platforms = SWITCH_PLATFORMS
else:
return False
for component in platforms:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True

View File

@ -8,24 +8,27 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
from homeassistant.helpers.device_registry import format_mac
# pylint: disable=unused-import
from .const import DOMAIN
from .gateway import ConnectXiaomiGateway
from .const import (
CONF_DEVICE,
CONF_FLOW_TYPE,
CONF_GATEWAY,
CONF_MAC,
CONF_MODEL,
DOMAIN,
MODELS_GATEWAY,
MODELS_SWITCH,
)
from .device import ConnectXiaomiDevice
_LOGGER = logging.getLogger(__name__)
CONF_FLOW_TYPE = "config_flow_device"
CONF_GATEWAY = "gateway"
DEFAULT_GATEWAY_NAME = "Xiaomi Gateway"
ZEROCONF_GATEWAY = "lumi-gateway"
ZEROCONF_ACPARTNER = "lumi-acpartner"
DEFAULT_DEVICE_NAME = "Xiaomi Device"
GATEWAY_SETTINGS = {
DEVICE_SETTINGS = {
vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)),
vol.Optional(CONF_NAME, default=DEFAULT_GATEWAY_NAME): str,
}
GATEWAY_CONFIG = vol.Schema({vol.Required(CONF_HOST): str}).extend(GATEWAY_SETTINGS)
CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_GATEWAY, default=False): bool})
DEVICE_CONFIG = vol.Schema({vol.Required(CONF_HOST): str}).extend(DEVICE_SETTINGS)
class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@ -38,19 +41,13 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Initialize."""
self.host = None
async def async_step_import(self, conf: dict):
"""Import a configuration from config.yaml."""
return await self.async_step_device(user_input=conf)
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
errors = {}
if user_input is not None:
# Check which device needs to be connected.
if user_input[CONF_GATEWAY]:
return await self.async_step_gateway()
errors["base"] = "no_device_selected"
return self.async_show_form(
step_id="user", data_schema=CONFIG_SCHEMA, errors=errors
)
return await self.async_step_device()
async def async_step_zeroconf(self, discovery_info):
"""Handle zeroconf discovery."""
@ -62,16 +59,28 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="not_xiaomi_miio")
# Check which device is discovered.
if name.startswith(ZEROCONF_GATEWAY) or name.startswith(ZEROCONF_ACPARTNER):
unique_id = format_mac(mac_address)
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured({CONF_HOST: self.host})
for gateway_model in MODELS_GATEWAY:
if name.startswith(gateway_model.replace(".", "-")):
unique_id = format_mac(mac_address)
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured({CONF_HOST: self.host})
self.context.update(
{"title_placeholders": {"name": f"Gateway {self.host}"}}
)
self.context.update(
{"title_placeholders": {"name": f"Gateway {self.host}"}}
)
return await self.async_step_gateway()
return await self.async_step_device()
for switch_model in MODELS_SWITCH:
if name.startswith(switch_model.replace(".", "-")):
unique_id = format_mac(mac_address)
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured({CONF_HOST: self.host})
self.context.update(
{"title_placeholders": {"name": f"Miio Device {self.host}"}}
)
return await self.async_step_device()
# Discovered device is not yet supported
_LOGGER.debug(
@ -81,42 +90,63 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
)
return self.async_abort(reason="not_xiaomi_miio")
async def async_step_gateway(self, user_input=None):
"""Handle a flow initialized by the user to configure a gateway."""
async def async_step_device(self, user_input=None):
"""Handle a flow initialized by the user to configure a xiaomi miio device."""
errors = {}
if user_input is not None:
token = user_input[CONF_TOKEN]
if user_input.get(CONF_HOST):
self.host = user_input[CONF_HOST]
# Try to connect to a Xiaomi Gateway.
connect_gateway_class = ConnectXiaomiGateway(self.hass)
await connect_gateway_class.async_connect_gateway(self.host, token)
gateway_info = connect_gateway_class.gateway_info
# 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 gateway_info is not None:
mac = format_mac(gateway_info.mac_address)
unique_id = mac
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_NAME],
data={
CONF_FLOW_TYPE: CONF_GATEWAY,
CONF_HOST: self.host,
CONF_TOKEN: token,
"model": gateway_info.model,
"mac": mac,
},
)
if device_info is not None:
# Setup Gateways
for gateway_model in MODELS_GATEWAY:
if device_info.model.startswith(gateway_model):
mac = format_mac(device_info.mac_address)
unique_id = mac
await self.async_set_unique_id(unique_id)
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: device_info.model,
CONF_MAC: mac,
},
)
errors["base"] = "cannot_connect"
# Setup all other Miio Devices
name = user_input.get(CONF_NAME, DEFAULT_DEVICE_NAME)
if device_info.model in MODELS_SWITCH:
mac = format_mac(device_info.mac_address)
unique_id = mac
await self.async_set_unique_id(unique_id)
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: device_info.model,
CONF_MAC: mac,
},
)
errors["base"] = "unknown_device"
else:
errors["base"] = "cannot_connect"
if self.host:
schema = vol.Schema(GATEWAY_SETTINGS)
schema = vol.Schema(DEVICE_SETTINGS)
else:
schema = GATEWAY_CONFIG
schema = DEVICE_CONFIG
return self.async_show_form(
step_id="gateway", data_schema=schema, errors=errors
)
return self.async_show_form(step_id="device", data_schema=schema, errors=errors)

View File

@ -1,6 +1,27 @@
"""Constants for the Xiaomi Miio component."""
DOMAIN = "xiaomi_miio"
CONF_FLOW_TYPE = "config_flow_device"
CONF_GATEWAY = "gateway"
CONF_DEVICE = "device"
CONF_MODEL = "model"
CONF_MAC = "mac"
MODELS_GATEWAY = ["lumi.gateway", "lumi.acpartner"]
MODELS_SWITCH = [
"chuangmi.plug.v1",
"chuangmi.plug.v3",
"chuangmi.plug.hmi208",
"qmi.powerstrip.v1",
"zimi.powerstrip.v2",
"chuangmi.plug.m1",
"chuangmi.plug.m3",
"chuangmi.plug.v2",
"chuangmi.plug.hmi205",
"chuangmi.plug.hmi206",
"lumi.acpartner.v3",
]
# Fan Services
SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on"
SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off"

View File

@ -0,0 +1,87 @@
"""Code to handle a Xiaomi Device."""
import logging
from miio import Device, DeviceException
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import Entity
from .const import CONF_MAC, CONF_MODEL, DOMAIN
_LOGGER = logging.getLogger(__name__)
class ConnectXiaomiDevice:
"""Class to async connect to a Xiaomi Device."""
def __init__(self, hass):
"""Initialize the entity."""
self._hass = hass
self._device = None
self._device_info = None
@property
def device(self):
"""Return the class containing all connections to the device."""
return self._device
@property
def device_info(self):
"""Return the class containing device info."""
return self._device_info
async def async_connect_device(self, host, token):
"""Connect to the Xiaomi Device."""
_LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
try:
self._device = Device(host, token)
# get the device info
self._device_info = await self._hass.async_add_executor_job(
self._device.info
)
except DeviceException:
_LOGGER.error(
"DeviceException during setup of xiaomi device with host %s", host
)
return False
_LOGGER.debug(
"%s %s %s detected",
self._device_info.model,
self._device_info.firmware_version,
self._device_info.hardware_version,
)
return True
class XiaomiMiioEntity(Entity):
"""Representation of a base Xiaomi Miio Entity."""
def __init__(self, name, device, entry, unique_id):
"""Initialize the Xiaomi Miio Device."""
self._device = device
self._model = entry.data[CONF_MODEL]
self._mac = entry.data[CONF_MAC]
self._device_id = entry.unique_id
self._unique_id = unique_id
self._name = name
@property
def unique_id(self):
"""Return an unique ID."""
return self._unique_id
@property
def name(self):
"""Return the name of this entity, if any."""
return self._name
@property
def device_info(self):
"""Return the device info."""
return {
"connections": {(dr.CONNECTION_NETWORK_MAC, self._mac)},
"identifiers": {(DOMAIN, self._device_id)},
"manufacturer": "Xiaomi",
"name": self._name,
"model": self._model,
}

View File

@ -6,14 +6,8 @@ from functools import partial
import logging
from math import ceil
from miio import ( # pylint: disable=import-error
Ceil,
Device,
DeviceException,
PhilipsBulb,
PhilipsEyecare,
PhilipsMoonlight,
)
from miio import Ceil, DeviceException, PhilipsBulb, PhilipsEyecare, PhilipsMoonlight
from miio import Device # pylint: disable=import-error
from miio.gateway import (
GATEWAY_MODEL_AC_V1,
GATEWAY_MODEL_AC_V2,
@ -37,8 +31,9 @@ from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.util import color, dt
from .config_flow import CONF_FLOW_TYPE, CONF_GATEWAY
from .const import (
CONF_FLOW_TYPE,
CONF_GATEWAY,
DOMAIN,
SERVICE_EYECARE_MODE_OFF,
SERVICE_EYECARE_MODE_ON,

View File

@ -31,8 +31,7 @@ from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from .config_flow import CONF_FLOW_TYPE, CONF_GATEWAY
from .const import DOMAIN
from .const import CONF_FLOW_TYPE, CONF_GATEWAY, DOMAIN
from .gateway import XiaomiGatewayDevice
_LOGGER = logging.getLogger(__name__)

View File

@ -2,26 +2,19 @@
"config": {
"flow_title": "Xiaomi Miio: {name}",
"step": {
"user": {
"title": "Xiaomi Miio",
"description": "Select to which device you want to connect.",
"data": {
"gateway": "Connect to a Xiaomi Gateway"
}
},
"gateway": {
"title": "Connect to a Xiaomi Gateway",
"device": {
"title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway",
"description": "You will need the 32 character [%key:common::config_flow::data::api_token%], see https://www.home-assistant.io/integrations/vacuum.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.",
"data": {
"host": "[%key:common::config_flow::data::ip%]",
"token": "[%key:common::config_flow::data::api_token%]",
"name": "Name of the Gateway"
"name": "Name of the device"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_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."
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",

View File

@ -3,17 +3,13 @@ import asyncio
from functools import partial
import logging
from miio import ( # pylint: disable=import-error
AirConditioningCompanionV3,
ChuangmiPlug,
Device,
DeviceException,
PowerStrip,
)
from miio import AirConditioningCompanionV3 # pylint: disable=import-error
from miio import ChuangmiPlug, DeviceException, PowerStrip
from miio.powerstrip import PowerMode # pylint: disable=import-error
import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_MODE,
@ -21,23 +17,25 @@ from homeassistant.const import (
CONF_NAME,
CONF_TOKEN,
)
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from .const import (
CONF_DEVICE,
CONF_FLOW_TYPE,
CONF_MODEL,
DOMAIN,
SERVICE_SET_POWER_MODE,
SERVICE_SET_POWER_PRICE,
SERVICE_SET_WIFI_LED_OFF,
SERVICE_SET_WIFI_LED_ON,
)
from .device import XiaomiMiioEntity
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Xiaomi Miio Switch"
DATA_KEY = "switch.xiaomi_miio"
CONF_MODEL = "model"
MODEL_POWER_STRIP_V2 = "zimi.powerstrip.v2"
MODEL_PLUG_V3 = "chuangmi.plug.v3"
@ -114,119 +112,124 @@ SERVICE_TO_METHOD = {
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the switch from config."""
if DATA_KEY not in hass.data:
hass.data[DATA_KEY] = {}
"""Import Miio configuration from YAML."""
_LOGGER.warning(
"Loading Xiaomi Miio Switch via platform setup is deprecated. Please remove it from your configuration."
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
)
host = config[CONF_HOST]
token = config[CONF_TOKEN]
name = config[CONF_NAME]
model = config.get(CONF_MODEL)
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the switch from a config entry."""
entities = []
devices = []
unique_id = None
if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
if DATA_KEY not in hass.data:
hass.data[DATA_KEY] = {}
if model is None:
try:
miio_device = Device(host, token)
device_info = await hass.async_add_executor_job(miio_device.info)
model = device_info.model
unique_id = f"{model}-{device_info.mac_address}"
_LOGGER.info(
"%s %s %s detected",
model,
device_info.firmware_version,
device_info.hardware_version,
)
except DeviceException as ex:
raise PlatformNotReady from ex
host = config_entry.data[CONF_HOST]
token = config_entry.data[CONF_TOKEN]
name = config_entry.title
model = config_entry.data[CONF_MODEL]
unique_id = config_entry.unique_id
if model in ["chuangmi.plug.v1", "chuangmi.plug.v3", "chuangmi.plug.hmi208"]:
plug = ChuangmiPlug(host, token, model=model)
_LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
# The device has two switchable channels (mains and a USB port).
# A switch device per channel will be created.
for channel_usb in [True, False]:
device = ChuangMiPlugSwitch(name, plug, model, unique_id, channel_usb)
devices.append(device)
if model in ["chuangmi.plug.v1", "chuangmi.plug.v3", "chuangmi.plug.hmi208"]:
plug = ChuangmiPlug(host, token, model=model)
# The device has two switchable channels (mains and a USB port).
# A switch device per channel will be created.
for channel_usb in [True, False]:
if channel_usb:
unique_id_ch = f"{unique_id}-USB"
else:
unique_id_ch = f"{unique_id}-mains"
device = ChuangMiPlugSwitch(
name, plug, config_entry, unique_id_ch, channel_usb
)
entities.append(device)
hass.data[DATA_KEY][host] = device
elif model in ["qmi.powerstrip.v1", "zimi.powerstrip.v2"]:
plug = PowerStrip(host, token, model=model)
device = XiaomiPowerStripSwitch(name, plug, config_entry, unique_id)
entities.append(device)
hass.data[DATA_KEY][host] = device
elif model in [
"chuangmi.plug.m1",
"chuangmi.plug.m3",
"chuangmi.plug.v2",
"chuangmi.plug.hmi205",
"chuangmi.plug.hmi206",
]:
plug = ChuangmiPlug(host, token, model=model)
device = XiaomiPlugGenericSwitch(name, plug, config_entry, unique_id)
entities.append(device)
hass.data[DATA_KEY][host] = device
elif model in ["lumi.acpartner.v3"]:
plug = AirConditioningCompanionV3(host, token)
device = XiaomiAirConditioningCompanionSwitch(
name, plug, config_entry, unique_id
)
entities.append(device)
hass.data[DATA_KEY][host] = device
elif model in ["qmi.powerstrip.v1", "zimi.powerstrip.v2"]:
plug = PowerStrip(host, token, model=model)
device = XiaomiPowerStripSwitch(name, plug, model, unique_id)
devices.append(device)
hass.data[DATA_KEY][host] = device
elif model in [
"chuangmi.plug.m1",
"chuangmi.plug.m3",
"chuangmi.plug.v2",
"chuangmi.plug.hmi205",
"chuangmi.plug.hmi206",
]:
plug = ChuangmiPlug(host, token, model=model)
device = XiaomiPlugGenericSwitch(name, plug, model, unique_id)
devices.append(device)
hass.data[DATA_KEY][host] = device
elif model in ["lumi.acpartner.v3"]:
plug = AirConditioningCompanionV3(host, token)
device = XiaomiAirConditioningCompanionSwitch(name, plug, model, unique_id)
devices.append(device)
hass.data[DATA_KEY][host] = device
else:
_LOGGER.error(
"Unsupported device found! Please create an issue at "
"https://github.com/rytilahti/python-miio/issues "
"and provide the following data: %s",
model,
)
return False
async_add_entities(devices, update_before_add=True)
async def async_service_handler(service):
"""Map services to methods on XiaomiPlugGenericSwitch."""
method = SERVICE_TO_METHOD.get(service.service)
params = {
key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID
}
entity_ids = service.data.get(ATTR_ENTITY_ID)
if entity_ids:
devices = [
device
for device in hass.data[DATA_KEY].values()
if device.entity_id in entity_ids
]
else:
devices = hass.data[DATA_KEY].values()
_LOGGER.error(
"Unsupported device found! Please create an issue at "
"https://github.com/rytilahti/python-miio/issues "
"and provide the following data: %s",
model,
)
update_tasks = []
for device in devices:
if not hasattr(device, method["method"]):
continue
await getattr(device, method["method"])(**params)
update_tasks.append(device.async_update_ha_state(True))
async def async_service_handler(service):
"""Map services to methods on XiaomiPlugGenericSwitch."""
method = SERVICE_TO_METHOD.get(service.service)
params = {
key: value
for key, value in service.data.items()
if key != ATTR_ENTITY_ID
}
entity_ids = service.data.get(ATTR_ENTITY_ID)
if entity_ids:
devices = [
device
for device in hass.data[DATA_KEY].values()
if device.entity_id in entity_ids
]
else:
devices = hass.data[DATA_KEY].values()
if update_tasks:
await asyncio.wait(update_tasks)
update_tasks = []
for device in devices:
if not hasattr(device, method["method"]):
continue
await getattr(device, method["method"])(**params)
update_tasks.append(device.async_update_ha_state(True))
for plug_service in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[plug_service].get("schema", SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, plug_service, async_service_handler, schema=schema
)
if update_tasks:
await asyncio.wait(update_tasks)
for plug_service in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[plug_service].get("schema", SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, plug_service, async_service_handler, schema=schema
)
async_add_entities(entities, update_before_add=True)
class XiaomiPlugGenericSwitch(SwitchEntity):
class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity):
"""Representation of a Xiaomi Plug Generic."""
def __init__(self, name, plug, model, unique_id):
def __init__(self, name, device, entry, unique_id):
"""Initialize the plug switch."""
self._name = name
self._plug = plug
self._model = model
self._unique_id = unique_id
super().__init__(name, device, entry, unique_id)
self._icon = "mdi:power-socket"
self._available = False
@ -235,16 +238,6 @@ class XiaomiPlugGenericSwitch(SwitchEntity):
self._device_features = FEATURE_FLAGS_GENERIC
self._skip_update = False
@property
def unique_id(self):
"""Return an unique ID."""
return self._unique_id
@property
def name(self):
"""Return the name of the device if any."""
return self._name
@property
def icon(self):
"""Return the icon to use for device if any."""
@ -288,7 +281,7 @@ class XiaomiPlugGenericSwitch(SwitchEntity):
async def async_turn_on(self, **kwargs):
"""Turn the plug on."""
result = await self._try_command("Turning the plug on failed.", self._plug.on)
result = await self._try_command("Turning the plug on failed", self._device.on)
if result:
self._state = True
@ -296,7 +289,9 @@ class XiaomiPlugGenericSwitch(SwitchEntity):
async def async_turn_off(self, **kwargs):
"""Turn the plug off."""
result = await self._try_command("Turning the plug off failed.", self._plug.off)
result = await self._try_command(
"Turning the plug off failed", self._device.off
)
if result:
self._state = False
@ -310,7 +305,7 @@ class XiaomiPlugGenericSwitch(SwitchEntity):
return
try:
state = await self.hass.async_add_executor_job(self._plug.status)
state = await self.hass.async_add_executor_job(self._device.status)
_LOGGER.debug("Got new state: %s", state)
self._available = True
@ -328,7 +323,7 @@ class XiaomiPlugGenericSwitch(SwitchEntity):
return
await self._try_command(
"Turning the wifi led on failed.", self._plug.set_wifi_led, True
"Turning the wifi led on failed", self._device.set_wifi_led, True
)
async def async_set_wifi_led_off(self):
@ -337,7 +332,7 @@ class XiaomiPlugGenericSwitch(SwitchEntity):
return
await self._try_command(
"Turning the wifi led off failed.", self._plug.set_wifi_led, False
"Turning the wifi led off failed", self._device.set_wifi_led, False
)
async def async_set_power_price(self, price: int):
@ -346,8 +341,8 @@ class XiaomiPlugGenericSwitch(SwitchEntity):
return
await self._try_command(
"Setting the power price of the power strip failed.",
self._plug.set_power_price,
"Setting the power price of the power strip failed",
self._device.set_power_price,
price,
)
@ -383,7 +378,7 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch):
return
try:
state = await self.hass.async_add_executor_job(self._plug.status)
state = await self.hass.async_add_executor_job(self._device.status)
_LOGGER.debug("Got new state: %s", state)
self._available = True
@ -415,8 +410,8 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch):
return
await self._try_command(
"Setting the power mode of the power strip failed.",
self._plug.set_power_mode,
"Setting the power mode of the power strip failed",
self._device.set_power_mode,
PowerMode(mode),
)
@ -424,14 +419,14 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch):
class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch):
"""Representation of a Chuang Mi Plug V1 and V3."""
def __init__(self, name, plug, model, unique_id, channel_usb):
def __init__(self, name, plug, entry, unique_id, channel_usb):
"""Initialize the plug switch."""
name = f"{name} USB" if channel_usb else name
if unique_id is not None and channel_usb:
unique_id = f"{unique_id}-usb"
super().__init__(name, plug, model, unique_id)
super().__init__(name, plug, entry, unique_id)
self._channel_usb = channel_usb
if self._model == MODEL_PLUG_V3:
@ -444,11 +439,11 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch):
"""Turn a channel on."""
if self._channel_usb:
result = await self._try_command(
"Turning the plug on failed.", self._plug.usb_on
"Turning the plug on failed", self._device.usb_on
)
else:
result = await self._try_command(
"Turning the plug on failed.", self._plug.on
"Turning the plug on failed", self._device.on
)
if result:
@ -459,11 +454,11 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch):
"""Turn a channel off."""
if self._channel_usb:
result = await self._try_command(
"Turning the plug on failed.", self._plug.usb_off
"Turning the plug off failed", self._device.usb_off
)
else:
result = await self._try_command(
"Turning the plug on failed.", self._plug.off
"Turning the plug off failed", self._device.off
)
if result:
@ -478,7 +473,7 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch):
return
try:
state = await self.hass.async_add_executor_job(self._plug.status)
state = await self.hass.async_add_executor_job(self._device.status)
_LOGGER.debug("Got new state: %s", state)
self._available = True
@ -513,7 +508,7 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch):
async def async_turn_on(self, **kwargs):
"""Turn the socket on."""
result = await self._try_command(
"Turning the socket on failed.", self._plug.socket_on
"Turning the socket on failed", self._device.socket_on
)
if result:
@ -523,7 +518,7 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch):
async def async_turn_off(self, **kwargs):
"""Turn the socket off."""
result = await self._try_command(
"Turning the socket off failed.", self._plug.socket_off
"Turning the socket off failed", self._device.socket_off
)
if result:
@ -538,7 +533,7 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch):
return
try:
state = await self.hass.async_add_executor_job(self._plug.status)
state = await self.hass.async_add_executor_job(self._device.status)
_LOGGER.debug("Got new state: %s", state)
self._available = True

View File

@ -6,25 +6,18 @@
},
"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."
},
"flow_title": "Xiaomi Miio: {name}",
"step": {
"gateway": {
"device": {
"data": {
"host": "IP Address",
"name": "Name of the Gateway",
"token": "API Token"
"host": "IP Address",
"name": "Name of the device",
"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"
"title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway"
}
}
}

View File

@ -5,7 +5,11 @@ from miio import DeviceException
from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.components.xiaomi_miio import config_flow, const
from homeassistant.components.xiaomi_miio import const
from homeassistant.components.xiaomi_miio.config_flow import (
DEFAULT_DEVICE_NAME,
DEFAULT_GATEWAY_NAME,
)
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
ZEROCONF_NAME = "name"
@ -15,7 +19,7 @@ ZEROCONF_MAC = "mac"
TEST_HOST = "1.2.3.4"
TEST_TOKEN = "12345678901234567890123456789012"
TEST_NAME = "Test_Gateway"
TEST_MODEL = "model5"
TEST_MODEL = const.MODELS_GATEWAY[0]
TEST_MAC = "ab:cd:ef:gh:ij:kl"
TEST_GATEWAY_ID = TEST_MAC
TEST_HARDWARE_VERSION = "AB123"
@ -40,26 +44,6 @@ def get_mock_info(
return gateway_info
async def test_config_flow_step_user_no_device(hass):
"""Test config flow, user step with no device selected."""
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{},
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {"base": "no_device_selected"}
async def test_config_flow_step_gateway_connect_error(hass):
"""Test config flow, gateway connection error."""
result = await hass.config_entries.flow.async_init(
@ -67,29 +51,20 @@ async def test_config_flow_step_gateway_connect_error(hass):
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{config_flow.CONF_GATEWAY: True},
)
assert result["type"] == "form"
assert result["step_id"] == "gateway"
assert result["step_id"] == "device"
assert result["errors"] == {}
with patch(
"homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.info",
"homeassistant.components.xiaomi_miio.device.Device.info",
side_effect=DeviceException({}),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: TEST_HOST, CONF_NAME: TEST_NAME, CONF_TOKEN: TEST_TOKEN},
{CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN},
)
assert result["type"] == "form"
assert result["step_id"] == "gateway"
assert result["step_id"] == "device"
assert result["errors"] == {"base": "cannot_connect"}
@ -100,42 +75,30 @@ async def test_config_flow_gateway_success(hass):
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{config_flow.CONF_GATEWAY: True},
)
assert result["type"] == "form"
assert result["step_id"] == "gateway"
assert result["step_id"] == "device"
assert result["errors"] == {}
mock_info = get_mock_info()
with patch(
"homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.info",
"homeassistant.components.xiaomi_miio.device.Device.info",
return_value=mock_info,
), patch(
"homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.discover_devices",
return_value=TEST_SUB_DEVICE_LIST,
), 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_NAME: TEST_NAME, CONF_TOKEN: TEST_TOKEN},
{CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN},
)
assert result["type"] == "create_entry"
assert result["title"] == TEST_NAME
assert result["title"] == DEFAULT_GATEWAY_NAME
assert result["data"] == {
config_flow.CONF_FLOW_TYPE: config_flow.CONF_GATEWAY,
const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
"model": TEST_MODEL,
"mac": TEST_MAC,
const.CONF_MODEL: TEST_MODEL,
const.CONF_MAC: TEST_MAC,
}
@ -152,33 +115,30 @@ async def test_zeroconf_gateway_success(hass):
)
assert result["type"] == "form"
assert result["step_id"] == "gateway"
assert result["step_id"] == "device"
assert result["errors"] == {}
mock_info = get_mock_info()
with patch(
"homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.info",
"homeassistant.components.xiaomi_miio.device.Device.info",
return_value=mock_info,
), patch(
"homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.discover_devices",
return_value=TEST_SUB_DEVICE_LIST,
), patch(
"homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_NAME: TEST_NAME, CONF_TOKEN: TEST_TOKEN},
{CONF_TOKEN: TEST_TOKEN},
)
assert result["type"] == "create_entry"
assert result["title"] == TEST_NAME
assert result["title"] == DEFAULT_GATEWAY_NAME
assert result["data"] == {
config_flow.CONF_FLOW_TYPE: config_flow.CONF_GATEWAY,
const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
"model": TEST_MODEL,
"mac": TEST_MAC,
const.CONF_MODEL: TEST_MODEL,
const.CONF_MAC: TEST_MAC,
}
@ -218,3 +178,167 @@ async def test_zeroconf_missing_data(hass):
assert result["type"] == "abort"
assert result["reason"] == "not_xiaomi_miio"
async def test_config_flow_step_device_connect_error(hass):
"""Test config flow, device connection 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"] == "device"
assert result["errors"] == {}
with patch(
"homeassistant.components.xiaomi_miio.device.Device.info",
side_effect=DeviceException({}),
):
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"] == "device"
assert result["errors"] == {"base": "cannot_connect"}
async def test_config_flow_step_unknown_device(hass):
"""Test config flow, unknown device 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"] == "device"
assert result["errors"] == {}
mock_info = get_mock_info(model="UNKNOWN")
with patch(
"homeassistant.components.xiaomi_miio.device.Device.info",
return_value=mock_info,
):
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"] == "device"
assert result["errors"] == {"base": "unknown_device"}
async def test_import_flow_success(hass):
"""Test a successful import form yaml for a device."""
mock_info = get_mock_info(model=const.MODELS_SWITCH[0])
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,
context={"source": config_entries.SOURCE_IMPORT},
data={CONF_NAME: TEST_NAME, CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN},
)
assert result["type"] == "create_entry"
assert result["title"] == TEST_NAME
assert result["data"] == {
const.CONF_FLOW_TYPE: const.CONF_DEVICE,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: const.MODELS_SWITCH[0],
const.CONF_MAC: TEST_MAC,
}
async def config_flow_device_success(hass, model_to_test):
"""Test a successful config flow for a device (base class)."""
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "device"
assert result["errors"] == {}
mock_info = get_mock_info(model=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"],
{CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN},
)
assert result["type"] == "create_entry"
assert result["title"] == DEFAULT_DEVICE_NAME
assert result["data"] == {
const.CONF_FLOW_TYPE: const.CONF_DEVICE,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: model_to_test,
const.CONF_MAC: TEST_MAC,
}
async def zeroconf_device_success(hass, zeroconf_name_to_test, model_to_test):
"""Test a successful zeroconf discovery of a device (base class)."""
result = await hass.config_entries.flow.async_init(
const.DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
zeroconf.ATTR_HOST: TEST_HOST,
ZEROCONF_NAME: zeroconf_name_to_test,
ZEROCONF_PROP: {ZEROCONF_MAC: TEST_MAC},
},
)
assert result["type"] == "form"
assert result["step_id"] == "device"
assert result["errors"] == {}
mock_info = get_mock_info(model=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"],
{CONF_TOKEN: TEST_TOKEN},
)
assert result["type"] == "create_entry"
assert result["title"] == DEFAULT_DEVICE_NAME
assert result["data"] == {
const.CONF_FLOW_TYPE: const.CONF_DEVICE,
CONF_HOST: TEST_HOST,
CONF_TOKEN: TEST_TOKEN,
const.CONF_MODEL: model_to_test,
const.CONF_MAC: TEST_MAC,
}
async def test_config_flow_plug_success(hass):
"""Test a successful config flow for a plug."""
test_plug_model = const.MODELS_SWITCH[0]
await config_flow_device_success(hass, test_plug_model)
async def test_zeroconf_plug_success(hass):
"""Test a successful zeroconf discovery of a plug."""
test_plug_model = const.MODELS_SWITCH[0]
test_zeroconf_name = const.MODELS_SWITCH[0].replace(".", "-")
await zeroconf_device_success(hass, test_zeroconf_name, test_plug_model)