mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 14:17:45 +00:00
Add config flow to Xiaomi Miio switch (#46179)
This commit is contained in:
parent
68809e9f43
commit
2f9fda73f4
@ -1085,6 +1085,7 @@ omit =
|
|||||||
homeassistant/components/xiaomi_miio/__init__.py
|
homeassistant/components/xiaomi_miio/__init__.py
|
||||||
homeassistant/components/xiaomi_miio/air_quality.py
|
homeassistant/components/xiaomi_miio/air_quality.py
|
||||||
homeassistant/components/xiaomi_miio/alarm_control_panel.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/device_tracker.py
|
||||||
homeassistant/components/xiaomi_miio/fan.py
|
homeassistant/components/xiaomi_miio/fan.py
|
||||||
homeassistant/components/xiaomi_miio/gateway.py
|
homeassistant/components/xiaomi_miio/gateway.py
|
||||||
|
@ -3,11 +3,18 @@ from homeassistant import config_entries, core
|
|||||||
from homeassistant.const import CONF_HOST, CONF_TOKEN
|
from homeassistant.const import CONF_HOST, CONF_TOKEN
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
from .config_flow import CONF_FLOW_TYPE, CONF_GATEWAY
|
from .const import (
|
||||||
from .const import DOMAIN
|
CONF_DEVICE,
|
||||||
|
CONF_FLOW_TYPE,
|
||||||
|
CONF_GATEWAY,
|
||||||
|
CONF_MODEL,
|
||||||
|
DOMAIN,
|
||||||
|
MODELS_SWITCH,
|
||||||
|
)
|
||||||
from .gateway import ConnectXiaomiGateway
|
from .gateway import ConnectXiaomiGateway
|
||||||
|
|
||||||
GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor", "light"]
|
GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor", "light"]
|
||||||
|
SWITCH_PLATFORMS = ["switch"]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: core.HomeAssistant, config: dict):
|
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
|
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
|
||||||
):
|
):
|
||||||
"""Set up the Xiaomi Miio components from a config entry."""
|
"""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 entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY:
|
||||||
if not await async_setup_gateway_entry(hass, entry):
|
if not await async_setup_gateway_entry(hass, entry):
|
||||||
return False
|
return False
|
||||||
|
if entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
|
||||||
|
if not await async_setup_device_entry(hass, entry):
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -67,3 +77,23 @@ async def async_setup_gateway_entry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return True
|
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
|
||||||
|
@ -8,24 +8,27 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
|
|||||||
from homeassistant.helpers.device_registry import format_mac
|
from homeassistant.helpers.device_registry import format_mac
|
||||||
|
|
||||||
# pylint: disable=unused-import
|
# pylint: disable=unused-import
|
||||||
from .const import DOMAIN
|
from .const import (
|
||||||
from .gateway import ConnectXiaomiGateway
|
CONF_DEVICE,
|
||||||
|
CONF_FLOW_TYPE,
|
||||||
|
CONF_GATEWAY,
|
||||||
|
CONF_MAC,
|
||||||
|
CONF_MODEL,
|
||||||
|
DOMAIN,
|
||||||
|
MODELS_GATEWAY,
|
||||||
|
MODELS_SWITCH,
|
||||||
|
)
|
||||||
|
from .device import ConnectXiaomiDevice
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_FLOW_TYPE = "config_flow_device"
|
|
||||||
CONF_GATEWAY = "gateway"
|
|
||||||
DEFAULT_GATEWAY_NAME = "Xiaomi Gateway"
|
DEFAULT_GATEWAY_NAME = "Xiaomi Gateway"
|
||||||
ZEROCONF_GATEWAY = "lumi-gateway"
|
DEFAULT_DEVICE_NAME = "Xiaomi Device"
|
||||||
ZEROCONF_ACPARTNER = "lumi-acpartner"
|
|
||||||
|
|
||||||
GATEWAY_SETTINGS = {
|
DEVICE_SETTINGS = {
|
||||||
vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)),
|
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)
|
DEVICE_CONFIG = vol.Schema({vol.Required(CONF_HOST): str}).extend(DEVICE_SETTINGS)
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_GATEWAY, default=False): bool})
|
|
||||||
|
|
||||||
|
|
||||||
class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
@ -38,19 +41,13 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
self.host = None
|
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):
|
async def async_step_user(self, user_input=None):
|
||||||
"""Handle a flow initialized by the user."""
|
"""Handle a flow initialized by the user."""
|
||||||
errors = {}
|
return await self.async_step_device()
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_zeroconf(self, discovery_info):
|
async def async_step_zeroconf(self, discovery_info):
|
||||||
"""Handle zeroconf discovery."""
|
"""Handle zeroconf discovery."""
|
||||||
@ -62,16 +59,28 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
return self.async_abort(reason="not_xiaomi_miio")
|
return self.async_abort(reason="not_xiaomi_miio")
|
||||||
|
|
||||||
# Check which device is discovered.
|
# Check which device is discovered.
|
||||||
if name.startswith(ZEROCONF_GATEWAY) or name.startswith(ZEROCONF_ACPARTNER):
|
for gateway_model in MODELS_GATEWAY:
|
||||||
unique_id = format_mac(mac_address)
|
if name.startswith(gateway_model.replace(".", "-")):
|
||||||
await self.async_set_unique_id(unique_id)
|
unique_id = format_mac(mac_address)
|
||||||
self._abort_if_unique_id_configured({CONF_HOST: self.host})
|
await self.async_set_unique_id(unique_id)
|
||||||
|
self._abort_if_unique_id_configured({CONF_HOST: self.host})
|
||||||
|
|
||||||
self.context.update(
|
self.context.update(
|
||||||
{"title_placeholders": {"name": f"Gateway {self.host}"}}
|
{"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
|
# Discovered device is not yet supported
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
@ -81,42 +90,63 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
return self.async_abort(reason="not_xiaomi_miio")
|
return self.async_abort(reason="not_xiaomi_miio")
|
||||||
|
|
||||||
async def async_step_gateway(self, user_input=None):
|
async def async_step_device(self, user_input=None):
|
||||||
"""Handle a flow initialized by the user to configure a gateway."""
|
"""Handle a flow initialized by the user to configure a xiaomi miio device."""
|
||||||
errors = {}
|
errors = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
token = user_input[CONF_TOKEN]
|
token = user_input[CONF_TOKEN]
|
||||||
if user_input.get(CONF_HOST):
|
if user_input.get(CONF_HOST):
|
||||||
self.host = user_input[CONF_HOST]
|
self.host = user_input[CONF_HOST]
|
||||||
|
|
||||||
# Try to connect to a Xiaomi Gateway.
|
# Try to connect to a Xiaomi Device.
|
||||||
connect_gateway_class = ConnectXiaomiGateway(self.hass)
|
connect_device_class = ConnectXiaomiDevice(self.hass)
|
||||||
await connect_gateway_class.async_connect_gateway(self.host, token)
|
await connect_device_class.async_connect_device(self.host, token)
|
||||||
gateway_info = connect_gateway_class.gateway_info
|
device_info = connect_device_class.device_info
|
||||||
|
|
||||||
if gateway_info is not None:
|
if device_info is not None:
|
||||||
mac = format_mac(gateway_info.mac_address)
|
# Setup Gateways
|
||||||
unique_id = mac
|
for gateway_model in MODELS_GATEWAY:
|
||||||
await self.async_set_unique_id(unique_id)
|
if device_info.model.startswith(gateway_model):
|
||||||
self._abort_if_unique_id_configured()
|
mac = format_mac(device_info.mac_address)
|
||||||
return self.async_create_entry(
|
unique_id = mac
|
||||||
title=user_input[CONF_NAME],
|
await self.async_set_unique_id(unique_id)
|
||||||
data={
|
self._abort_if_unique_id_configured()
|
||||||
CONF_FLOW_TYPE: CONF_GATEWAY,
|
return self.async_create_entry(
|
||||||
CONF_HOST: self.host,
|
title=DEFAULT_GATEWAY_NAME,
|
||||||
CONF_TOKEN: token,
|
data={
|
||||||
"model": gateway_info.model,
|
CONF_FLOW_TYPE: CONF_GATEWAY,
|
||||||
"mac": mac,
|
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:
|
if self.host:
|
||||||
schema = vol.Schema(GATEWAY_SETTINGS)
|
schema = vol.Schema(DEVICE_SETTINGS)
|
||||||
else:
|
else:
|
||||||
schema = GATEWAY_CONFIG
|
schema = DEVICE_CONFIG
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(step_id="device", data_schema=schema, errors=errors)
|
||||||
step_id="gateway", data_schema=schema, errors=errors
|
|
||||||
)
|
|
||||||
|
@ -1,6 +1,27 @@
|
|||||||
"""Constants for the Xiaomi Miio component."""
|
"""Constants for the Xiaomi Miio component."""
|
||||||
DOMAIN = "xiaomi_miio"
|
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
|
# Fan Services
|
||||||
SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on"
|
SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on"
|
||||||
SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off"
|
SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off"
|
||||||
|
87
homeassistant/components/xiaomi_miio/device.py
Normal file
87
homeassistant/components/xiaomi_miio/device.py
Normal 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,
|
||||||
|
}
|
@ -6,14 +6,8 @@ from functools import partial
|
|||||||
import logging
|
import logging
|
||||||
from math import ceil
|
from math import ceil
|
||||||
|
|
||||||
from miio import ( # pylint: disable=import-error
|
from miio import Ceil, DeviceException, PhilipsBulb, PhilipsEyecare, PhilipsMoonlight
|
||||||
Ceil,
|
from miio import Device # pylint: disable=import-error
|
||||||
Device,
|
|
||||||
DeviceException,
|
|
||||||
PhilipsBulb,
|
|
||||||
PhilipsEyecare,
|
|
||||||
PhilipsMoonlight,
|
|
||||||
)
|
|
||||||
from miio.gateway import (
|
from miio.gateway import (
|
||||||
GATEWAY_MODEL_AC_V1,
|
GATEWAY_MODEL_AC_V1,
|
||||||
GATEWAY_MODEL_AC_V2,
|
GATEWAY_MODEL_AC_V2,
|
||||||
@ -37,8 +31,9 @@ from homeassistant.exceptions import PlatformNotReady
|
|||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util import color, dt
|
from homeassistant.util import color, dt
|
||||||
|
|
||||||
from .config_flow import CONF_FLOW_TYPE, CONF_GATEWAY
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_FLOW_TYPE,
|
||||||
|
CONF_GATEWAY,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_EYECARE_MODE_OFF,
|
SERVICE_EYECARE_MODE_OFF,
|
||||||
SERVICE_EYECARE_MODE_ON,
|
SERVICE_EYECARE_MODE_ON,
|
||||||
|
@ -31,8 +31,7 @@ from homeassistant.exceptions import PlatformNotReady
|
|||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from .config_flow import CONF_FLOW_TYPE, CONF_GATEWAY
|
from .const import CONF_FLOW_TYPE, CONF_GATEWAY, DOMAIN
|
||||||
from .const import DOMAIN
|
|
||||||
from .gateway import XiaomiGatewayDevice
|
from .gateway import XiaomiGatewayDevice
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -2,26 +2,19 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"flow_title": "Xiaomi Miio: {name}",
|
"flow_title": "Xiaomi Miio: {name}",
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"device": {
|
||||||
"title": "Xiaomi Miio",
|
"title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway",
|
||||||
"description": "Select to which device you want to connect.",
|
|
||||||
"data": {
|
|
||||||
"gateway": "Connect to a Xiaomi Gateway"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"gateway": {
|
|
||||||
"title": "Connect to a 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.",
|
"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": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::ip%]",
|
"host": "[%key:common::config_flow::data::ip%]",
|
||||||
"token": "[%key:common::config_flow::data::api_token%]",
|
"token": "[%key:common::config_flow::data::api_token%]",
|
||||||
"name": "Name of the Gateway"
|
"name": "Name of the device"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"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": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
|
@ -3,17 +3,13 @@ import asyncio
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from miio import ( # pylint: disable=import-error
|
from miio import AirConditioningCompanionV3 # pylint: disable=import-error
|
||||||
AirConditioningCompanionV3,
|
from miio import ChuangmiPlug, DeviceException, PowerStrip
|
||||||
ChuangmiPlug,
|
|
||||||
Device,
|
|
||||||
DeviceException,
|
|
||||||
PowerStrip,
|
|
||||||
)
|
|
||||||
from miio.powerstrip import PowerMode # pylint: disable=import-error
|
from miio.powerstrip import PowerMode # pylint: disable=import-error
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
|
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
ATTR_MODE,
|
ATTR_MODE,
|
||||||
@ -21,23 +17,25 @@ from homeassistant.const import (
|
|||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_TOKEN,
|
CONF_TOKEN,
|
||||||
)
|
)
|
||||||
from homeassistant.exceptions import PlatformNotReady
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_DEVICE,
|
||||||
|
CONF_FLOW_TYPE,
|
||||||
|
CONF_MODEL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_SET_POWER_MODE,
|
SERVICE_SET_POWER_MODE,
|
||||||
SERVICE_SET_POWER_PRICE,
|
SERVICE_SET_POWER_PRICE,
|
||||||
SERVICE_SET_WIFI_LED_OFF,
|
SERVICE_SET_WIFI_LED_OFF,
|
||||||
SERVICE_SET_WIFI_LED_ON,
|
SERVICE_SET_WIFI_LED_ON,
|
||||||
)
|
)
|
||||||
|
from .device import XiaomiMiioEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NAME = "Xiaomi Miio Switch"
|
DEFAULT_NAME = "Xiaomi Miio Switch"
|
||||||
DATA_KEY = "switch.xiaomi_miio"
|
DATA_KEY = "switch.xiaomi_miio"
|
||||||
|
|
||||||
CONF_MODEL = "model"
|
|
||||||
MODEL_POWER_STRIP_V2 = "zimi.powerstrip.v2"
|
MODEL_POWER_STRIP_V2 = "zimi.powerstrip.v2"
|
||||||
MODEL_PLUG_V3 = "chuangmi.plug.v3"
|
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):
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
"""Set up the switch from config."""
|
"""Import Miio configuration from YAML."""
|
||||||
if DATA_KEY not in hass.data:
|
_LOGGER.warning(
|
||||||
hass.data[DATA_KEY] = {}
|
"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 = []
|
if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
|
||||||
unique_id = None
|
if DATA_KEY not in hass.data:
|
||||||
|
hass.data[DATA_KEY] = {}
|
||||||
|
|
||||||
if model is None:
|
host = config_entry.data[CONF_HOST]
|
||||||
try:
|
token = config_entry.data[CONF_TOKEN]
|
||||||
miio_device = Device(host, token)
|
name = config_entry.title
|
||||||
device_info = await hass.async_add_executor_job(miio_device.info)
|
model = config_entry.data[CONF_MODEL]
|
||||||
model = device_info.model
|
unique_id = config_entry.unique_id
|
||||||
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
|
|
||||||
|
|
||||||
if model in ["chuangmi.plug.v1", "chuangmi.plug.v3", "chuangmi.plug.hmi208"]:
|
_LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
|
||||||
plug = ChuangmiPlug(host, token, model=model)
|
|
||||||
|
|
||||||
# The device has two switchable channels (mains and a USB port).
|
if model in ["chuangmi.plug.v1", "chuangmi.plug.v3", "chuangmi.plug.hmi208"]:
|
||||||
# A switch device per channel will be created.
|
plug = ChuangmiPlug(host, token, model=model)
|
||||||
for channel_usb in [True, False]:
|
|
||||||
device = ChuangMiPlugSwitch(name, plug, model, unique_id, channel_usb)
|
# The device has two switchable channels (mains and a USB port).
|
||||||
devices.append(device)
|
# 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
|
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:
|
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 = []
|
async def async_service_handler(service):
|
||||||
for device in devices:
|
"""Map services to methods on XiaomiPlugGenericSwitch."""
|
||||||
if not hasattr(device, method["method"]):
|
method = SERVICE_TO_METHOD.get(service.service)
|
||||||
continue
|
params = {
|
||||||
await getattr(device, method["method"])(**params)
|
key: value
|
||||||
update_tasks.append(device.async_update_ha_state(True))
|
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:
|
update_tasks = []
|
||||||
await asyncio.wait(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:
|
if update_tasks:
|
||||||
schema = SERVICE_TO_METHOD[plug_service].get("schema", SERVICE_SCHEMA)
|
await asyncio.wait(update_tasks)
|
||||||
hass.services.async_register(
|
|
||||||
DOMAIN, plug_service, async_service_handler, schema=schema
|
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."""
|
"""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."""
|
"""Initialize the plug switch."""
|
||||||
self._name = name
|
super().__init__(name, device, entry, unique_id)
|
||||||
self._plug = plug
|
|
||||||
self._model = model
|
|
||||||
self._unique_id = unique_id
|
|
||||||
|
|
||||||
self._icon = "mdi:power-socket"
|
self._icon = "mdi:power-socket"
|
||||||
self._available = False
|
self._available = False
|
||||||
@ -235,16 +238,6 @@ class XiaomiPlugGenericSwitch(SwitchEntity):
|
|||||||
self._device_features = FEATURE_FLAGS_GENERIC
|
self._device_features = FEATURE_FLAGS_GENERIC
|
||||||
self._skip_update = False
|
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
|
@property
|
||||||
def icon(self):
|
def icon(self):
|
||||||
"""Return the icon to use for device if any."""
|
"""Return the icon to use for device if any."""
|
||||||
@ -288,7 +281,7 @@ class XiaomiPlugGenericSwitch(SwitchEntity):
|
|||||||
|
|
||||||
async def async_turn_on(self, **kwargs):
|
async def async_turn_on(self, **kwargs):
|
||||||
"""Turn the plug on."""
|
"""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:
|
if result:
|
||||||
self._state = True
|
self._state = True
|
||||||
@ -296,7 +289,9 @@ class XiaomiPlugGenericSwitch(SwitchEntity):
|
|||||||
|
|
||||||
async def async_turn_off(self, **kwargs):
|
async def async_turn_off(self, **kwargs):
|
||||||
"""Turn the plug off."""
|
"""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:
|
if result:
|
||||||
self._state = False
|
self._state = False
|
||||||
@ -310,7 +305,7 @@ class XiaomiPlugGenericSwitch(SwitchEntity):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
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)
|
_LOGGER.debug("Got new state: %s", state)
|
||||||
|
|
||||||
self._available = True
|
self._available = True
|
||||||
@ -328,7 +323,7 @@ class XiaomiPlugGenericSwitch(SwitchEntity):
|
|||||||
return
|
return
|
||||||
|
|
||||||
await self._try_command(
|
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):
|
async def async_set_wifi_led_off(self):
|
||||||
@ -337,7 +332,7 @@ class XiaomiPlugGenericSwitch(SwitchEntity):
|
|||||||
return
|
return
|
||||||
|
|
||||||
await self._try_command(
|
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):
|
async def async_set_power_price(self, price: int):
|
||||||
@ -346,8 +341,8 @@ class XiaomiPlugGenericSwitch(SwitchEntity):
|
|||||||
return
|
return
|
||||||
|
|
||||||
await self._try_command(
|
await self._try_command(
|
||||||
"Setting the power price of the power strip failed.",
|
"Setting the power price of the power strip failed",
|
||||||
self._plug.set_power_price,
|
self._device.set_power_price,
|
||||||
price,
|
price,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -383,7 +378,7 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
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)
|
_LOGGER.debug("Got new state: %s", state)
|
||||||
|
|
||||||
self._available = True
|
self._available = True
|
||||||
@ -415,8 +410,8 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch):
|
|||||||
return
|
return
|
||||||
|
|
||||||
await self._try_command(
|
await self._try_command(
|
||||||
"Setting the power mode of the power strip failed.",
|
"Setting the power mode of the power strip failed",
|
||||||
self._plug.set_power_mode,
|
self._device.set_power_mode,
|
||||||
PowerMode(mode),
|
PowerMode(mode),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -424,14 +419,14 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch):
|
|||||||
class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch):
|
class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch):
|
||||||
"""Representation of a Chuang Mi Plug V1 and V3."""
|
"""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."""
|
"""Initialize the plug switch."""
|
||||||
name = f"{name} USB" if channel_usb else name
|
name = f"{name} USB" if channel_usb else name
|
||||||
|
|
||||||
if unique_id is not None and channel_usb:
|
if unique_id is not None and channel_usb:
|
||||||
unique_id = f"{unique_id}-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
|
self._channel_usb = channel_usb
|
||||||
|
|
||||||
if self._model == MODEL_PLUG_V3:
|
if self._model == MODEL_PLUG_V3:
|
||||||
@ -444,11 +439,11 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch):
|
|||||||
"""Turn a channel on."""
|
"""Turn a channel on."""
|
||||||
if self._channel_usb:
|
if self._channel_usb:
|
||||||
result = await self._try_command(
|
result = await self._try_command(
|
||||||
"Turning the plug on failed.", self._plug.usb_on
|
"Turning the plug on failed", self._device.usb_on
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result = await self._try_command(
|
result = await self._try_command(
|
||||||
"Turning the plug on failed.", self._plug.on
|
"Turning the plug on failed", self._device.on
|
||||||
)
|
)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
@ -459,11 +454,11 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch):
|
|||||||
"""Turn a channel off."""
|
"""Turn a channel off."""
|
||||||
if self._channel_usb:
|
if self._channel_usb:
|
||||||
result = await self._try_command(
|
result = await self._try_command(
|
||||||
"Turning the plug on failed.", self._plug.usb_off
|
"Turning the plug off failed", self._device.usb_off
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result = await self._try_command(
|
result = await self._try_command(
|
||||||
"Turning the plug on failed.", self._plug.off
|
"Turning the plug off failed", self._device.off
|
||||||
)
|
)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
@ -478,7 +473,7 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
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)
|
_LOGGER.debug("Got new state: %s", state)
|
||||||
|
|
||||||
self._available = True
|
self._available = True
|
||||||
@ -513,7 +508,7 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch):
|
|||||||
async def async_turn_on(self, **kwargs):
|
async def async_turn_on(self, **kwargs):
|
||||||
"""Turn the socket on."""
|
"""Turn the socket on."""
|
||||||
result = await self._try_command(
|
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:
|
if result:
|
||||||
@ -523,7 +518,7 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch):
|
|||||||
async def async_turn_off(self, **kwargs):
|
async def async_turn_off(self, **kwargs):
|
||||||
"""Turn the socket off."""
|
"""Turn the socket off."""
|
||||||
result = await self._try_command(
|
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:
|
if result:
|
||||||
@ -538,7 +533,7 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
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)
|
_LOGGER.debug("Got new state: %s", state)
|
||||||
|
|
||||||
self._available = True
|
self._available = True
|
||||||
|
@ -6,25 +6,18 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "Failed to connect",
|
"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}",
|
"flow_title": "Xiaomi Miio: {name}",
|
||||||
"step": {
|
"step": {
|
||||||
"gateway": {
|
"device": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "IP Address",
|
"host": "IP Address",
|
||||||
"name": "Name of the Gateway",
|
"name": "Name of the device",
|
||||||
"token": "API Token"
|
"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.",
|
"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"
|
"title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway"
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"data": {
|
|
||||||
"gateway": "Connect to a Xiaomi Gateway"
|
|
||||||
},
|
|
||||||
"description": "Select to which device you want to connect.",
|
|
||||||
"title": "Xiaomi Miio"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,11 @@ from miio import DeviceException
|
|||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import zeroconf
|
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
|
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
|
||||||
|
|
||||||
ZEROCONF_NAME = "name"
|
ZEROCONF_NAME = "name"
|
||||||
@ -15,7 +19,7 @@ ZEROCONF_MAC = "mac"
|
|||||||
TEST_HOST = "1.2.3.4"
|
TEST_HOST = "1.2.3.4"
|
||||||
TEST_TOKEN = "12345678901234567890123456789012"
|
TEST_TOKEN = "12345678901234567890123456789012"
|
||||||
TEST_NAME = "Test_Gateway"
|
TEST_NAME = "Test_Gateway"
|
||||||
TEST_MODEL = "model5"
|
TEST_MODEL = const.MODELS_GATEWAY[0]
|
||||||
TEST_MAC = "ab:cd:ef:gh:ij:kl"
|
TEST_MAC = "ab:cd:ef:gh:ij:kl"
|
||||||
TEST_GATEWAY_ID = TEST_MAC
|
TEST_GATEWAY_ID = TEST_MAC
|
||||||
TEST_HARDWARE_VERSION = "AB123"
|
TEST_HARDWARE_VERSION = "AB123"
|
||||||
@ -40,26 +44,6 @@ def get_mock_info(
|
|||||||
return gateway_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):
|
async def test_config_flow_step_gateway_connect_error(hass):
|
||||||
"""Test config flow, gateway connection error."""
|
"""Test config flow, gateway connection error."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
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["type"] == "form"
|
||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "device"
|
||||||
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["errors"] == {}
|
assert result["errors"] == {}
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.info",
|
"homeassistant.components.xiaomi_miio.device.Device.info",
|
||||||
side_effect=DeviceException({}),
|
side_effect=DeviceException({}),
|
||||||
):
|
):
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
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["type"] == "form"
|
||||||
assert result["step_id"] == "gateway"
|
assert result["step_id"] == "device"
|
||||||
assert result["errors"] == {"base": "cannot_connect"}
|
assert result["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
@ -100,42 +75,30 @@ async def test_config_flow_gateway_success(hass):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] == "form"
|
assert result["type"] == "form"
|
||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "device"
|
||||||
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["errors"] == {}
|
assert result["errors"] == {}
|
||||||
|
|
||||||
mock_info = get_mock_info()
|
mock_info = get_mock_info()
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.info",
|
"homeassistant.components.xiaomi_miio.device.Device.info",
|
||||||
return_value=mock_info,
|
return_value=mock_info,
|
||||||
), patch(
|
|
||||||
"homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.discover_devices",
|
|
||||||
return_value=TEST_SUB_DEVICE_LIST,
|
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
|
"homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
|
||||||
):
|
):
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
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["type"] == "create_entry"
|
||||||
assert result["title"] == TEST_NAME
|
assert result["title"] == DEFAULT_GATEWAY_NAME
|
||||||
assert result["data"] == {
|
assert result["data"] == {
|
||||||
config_flow.CONF_FLOW_TYPE: config_flow.CONF_GATEWAY,
|
const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
|
||||||
CONF_HOST: TEST_HOST,
|
CONF_HOST: TEST_HOST,
|
||||||
CONF_TOKEN: TEST_TOKEN,
|
CONF_TOKEN: TEST_TOKEN,
|
||||||
"model": TEST_MODEL,
|
const.CONF_MODEL: TEST_MODEL,
|
||||||
"mac": TEST_MAC,
|
const.CONF_MAC: TEST_MAC,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -152,33 +115,30 @@ async def test_zeroconf_gateway_success(hass):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] == "form"
|
assert result["type"] == "form"
|
||||||
assert result["step_id"] == "gateway"
|
assert result["step_id"] == "device"
|
||||||
assert result["errors"] == {}
|
assert result["errors"] == {}
|
||||||
|
|
||||||
mock_info = get_mock_info()
|
mock_info = get_mock_info()
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.info",
|
"homeassistant.components.xiaomi_miio.device.Device.info",
|
||||||
return_value=mock_info,
|
return_value=mock_info,
|
||||||
), patch(
|
|
||||||
"homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.discover_devices",
|
|
||||||
return_value=TEST_SUB_DEVICE_LIST,
|
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
|
"homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
|
||||||
):
|
):
|
||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
{CONF_NAME: TEST_NAME, CONF_TOKEN: TEST_TOKEN},
|
{CONF_TOKEN: TEST_TOKEN},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] == "create_entry"
|
assert result["type"] == "create_entry"
|
||||||
assert result["title"] == TEST_NAME
|
assert result["title"] == DEFAULT_GATEWAY_NAME
|
||||||
assert result["data"] == {
|
assert result["data"] == {
|
||||||
config_flow.CONF_FLOW_TYPE: config_flow.CONF_GATEWAY,
|
const.CONF_FLOW_TYPE: const.CONF_GATEWAY,
|
||||||
CONF_HOST: TEST_HOST,
|
CONF_HOST: TEST_HOST,
|
||||||
CONF_TOKEN: TEST_TOKEN,
|
CONF_TOKEN: TEST_TOKEN,
|
||||||
"model": TEST_MODEL,
|
const.CONF_MODEL: TEST_MODEL,
|
||||||
"mac": TEST_MAC,
|
const.CONF_MAC: TEST_MAC,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -218,3 +178,167 @@ async def test_zeroconf_missing_data(hass):
|
|||||||
|
|
||||||
assert result["type"] == "abort"
|
assert result["type"] == "abort"
|
||||||
assert result["reason"] == "not_xiaomi_miio"
|
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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user