Add Xiaomi Miio vacuum config flow (#46669)

This commit is contained in:
starkillerOG 2021-02-22 13:01:02 +01:00 committed by GitHub
parent 23c2bd4e69
commit 338c07a56b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 278 additions and 189 deletions

View File

@ -17,6 +17,7 @@ from .const import (
DOMAIN, DOMAIN,
KEY_COORDINATOR, KEY_COORDINATOR,
MODELS_SWITCH, MODELS_SWITCH,
MODELS_VACUUM,
) )
from .gateway import ConnectXiaomiGateway from .gateway import ConnectXiaomiGateway
@ -24,6 +25,7 @@ _LOGGER = logging.getLogger(__name__)
GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor", "light"] GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor", "light"]
SWITCH_PLATFORMS = ["switch"] SWITCH_PLATFORMS = ["switch"]
VACUUM_PLATFORMS = ["vacuum"]
async def async_setup(hass: core.HomeAssistant, config: dict): async def async_setup(hass: core.HomeAssistant, config: dict):
@ -117,9 +119,14 @@ async def async_setup_device_entry(
model = entry.data[CONF_MODEL] model = entry.data[CONF_MODEL]
# Identify platforms to setup # Identify platforms to setup
platforms = []
if model in MODELS_SWITCH: if model in MODELS_SWITCH:
platforms = SWITCH_PLATFORMS platforms = SWITCH_PLATFORMS
else: for vacuum_model in MODELS_VACUUM:
if model.startswith(vacuum_model):
platforms = VACUUM_PLATFORMS
if not platforms:
return False return False
for component in platforms: for component in platforms:

View File

@ -15,8 +15,9 @@ from .const import (
CONF_MAC, CONF_MAC,
CONF_MODEL, CONF_MODEL,
DOMAIN, DOMAIN,
MODELS_ALL,
MODELS_ALL_DEVICES,
MODELS_GATEWAY, MODELS_GATEWAY,
MODELS_SWITCH,
) )
from .device import ConnectXiaomiDevice from .device import ConnectXiaomiDevice
@ -29,6 +30,7 @@ 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)),
} }
DEVICE_CONFIG = vol.Schema({vol.Required(CONF_HOST): str}).extend(DEVICE_SETTINGS) DEVICE_CONFIG = vol.Schema({vol.Required(CONF_HOST): str}).extend(DEVICE_SETTINGS)
DEVICE_MODEL_CONFIG = {vol.Optional(CONF_MODEL): vol.In(MODELS_ALL)}
class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@ -40,6 +42,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self): def __init__(self):
"""Initialize.""" """Initialize."""
self.host = None self.host = None
self.mac = None
async def async_step_import(self, conf: dict): async def async_step_import(self, conf: dict):
"""Import a configuration from config.yaml.""" """Import a configuration from config.yaml."""
@ -53,15 +56,15 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle zeroconf discovery.""" """Handle zeroconf discovery."""
name = discovery_info.get("name") name = discovery_info.get("name")
self.host = discovery_info.get("host") self.host = discovery_info.get("host")
mac_address = discovery_info.get("properties", {}).get("mac") self.mac = discovery_info.get("properties", {}).get("mac")
if not name or not self.host or not mac_address: if not name or not self.host or not self.mac:
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.
for gateway_model in MODELS_GATEWAY: for gateway_model in MODELS_GATEWAY:
if name.startswith(gateway_model.replace(".", "-")): if name.startswith(gateway_model.replace(".", "-")):
unique_id = format_mac(mac_address) unique_id = format_mac(self.mac)
await self.async_set_unique_id(unique_id) await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured({CONF_HOST: self.host}) self._abort_if_unique_id_configured({CONF_HOST: self.host})
@ -70,9 +73,9 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
) )
return await self.async_step_device() return await self.async_step_device()
for switch_model in MODELS_SWITCH: for device_model in MODELS_ALL_DEVICES:
if name.startswith(switch_model.replace(".", "-")): if name.startswith(device_model.replace(".", "-")):
unique_id = format_mac(mac_address) unique_id = format_mac(self.mac)
await self.async_set_unique_id(unique_id) await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured({CONF_HOST: self.host}) self._abort_if_unique_id_configured({CONF_HOST: self.host})
@ -95,6 +98,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors = {} errors = {}
if user_input is not None: if user_input is not None:
token = user_input[CONF_TOKEN] token = user_input[CONF_TOKEN]
model = user_input.get(CONF_MODEL)
if user_input.get(CONF_HOST): if user_input.get(CONF_HOST):
self.host = user_input[CONF_HOST] self.host = user_input[CONF_HOST]
@ -103,12 +107,17 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
await connect_device_class.async_connect_device(self.host, token) await connect_device_class.async_connect_device(self.host, token)
device_info = connect_device_class.device_info device_info = connect_device_class.device_info
if device_info is not None: if model is None and device_info is not None:
model = device_info.model
if model is not None:
if self.mac is None and device_info is not None:
self.mac = format_mac(device_info.mac_address)
# Setup Gateways # Setup Gateways
for gateway_model in MODELS_GATEWAY: for gateway_model in MODELS_GATEWAY:
if device_info.model.startswith(gateway_model): if model.startswith(gateway_model):
mac = format_mac(device_info.mac_address) unique_id = self.mac
unique_id = mac
await self.async_set_unique_id(unique_id) await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return self.async_create_entry( return self.async_create_entry(
@ -117,29 +126,29 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
CONF_FLOW_TYPE: CONF_GATEWAY, CONF_FLOW_TYPE: CONF_GATEWAY,
CONF_HOST: self.host, CONF_HOST: self.host,
CONF_TOKEN: token, CONF_TOKEN: token,
CONF_MODEL: device_info.model, CONF_MODEL: model,
CONF_MAC: mac, CONF_MAC: self.mac,
}, },
) )
# Setup all other Miio Devices # Setup all other Miio Devices
name = user_input.get(CONF_NAME, DEFAULT_DEVICE_NAME) name = user_input.get(CONF_NAME, DEFAULT_DEVICE_NAME)
if device_info.model in MODELS_SWITCH: for device_model in MODELS_ALL_DEVICES:
mac = format_mac(device_info.mac_address) if model.startswith(device_model):
unique_id = mac unique_id = self.mac
await self.async_set_unique_id(unique_id) await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return self.async_create_entry( return self.async_create_entry(
title=name, title=name,
data={ data={
CONF_FLOW_TYPE: CONF_DEVICE, CONF_FLOW_TYPE: CONF_DEVICE,
CONF_HOST: self.host, CONF_HOST: self.host,
CONF_TOKEN: token, CONF_TOKEN: token,
CONF_MODEL: device_info.model, CONF_MODEL: model,
CONF_MAC: mac, CONF_MAC: self.mac,
}, },
) )
errors["base"] = "unknown_device" errors["base"] = "unknown_device"
else: else:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
@ -149,4 +158,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
else: else:
schema = DEVICE_CONFIG schema = DEVICE_CONFIG
if errors:
schema = schema.extend(DEVICE_MODEL_CONFIG)
return self.async_show_form(step_id="device", data_schema=schema, errors=errors) return self.async_show_form(step_id="device", data_schema=schema, errors=errors)

View File

@ -23,6 +23,10 @@ MODELS_SWITCH = [
"chuangmi.plug.hmi206", "chuangmi.plug.hmi206",
"lumi.acpartner.v3", "lumi.acpartner.v3",
] ]
MODELS_VACUUM = ["roborock.vacuum"]
MODELS_ALL_DEVICES = MODELS_SWITCH + MODELS_VACUUM
MODELS_ALL = MODELS_ALL_DEVICES + MODELS_GATEWAY
# Fan Services # Fan Services
SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on" SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on"

View File

@ -78,10 +78,14 @@ class XiaomiMiioEntity(Entity):
@property @property
def device_info(self): def device_info(self):
"""Return the device info.""" """Return the device info."""
return { device_info = {
"connections": {(dr.CONNECTION_NETWORK_MAC, self._mac)},
"identifiers": {(DOMAIN, self._device_id)}, "identifiers": {(DOMAIN, self._device_id)},
"manufacturer": "Xiaomi", "manufacturer": "Xiaomi",
"name": self._name, "name": self._name,
"model": self._model, "model": self._model,
} }
if self._mac is not None:
device_info["connections"] = {(dr.CONNECTION_NETWORK_MAC, self._mac)}
return device_info

View File

@ -8,7 +8,7 @@
"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 device" "model": "Device model (Optional)"
} }
} }
}, },

View File

@ -6,7 +6,6 @@
}, },
"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." "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}",
@ -14,27 +13,11 @@
"device": { "device": {
"data": { "data": {
"host": "IP Address", "host": "IP Address",
"name": "Name of the device", "token": "API Token",
"token": "API Token" "model": "Device model (Optional)"
}, },
"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 Miio Device or Xiaomi Gateway" "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway"
},
"gateway": {
"data": {
"host": "IP Address",
"name": "Name of the Gateway",
"token": "API Token"
},
"description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.",
"title": "Connect to a Xiaomi Gateway"
},
"user": {
"data": {
"gateway": "Connect to a Xiaomi Gateway"
},
"description": "Select to which device you want to connect.",
"title": "Xiaomi Miio"
} }
} }
} }

View File

@ -26,11 +26,15 @@ from homeassistant.components.vacuum import (
SUPPORT_STOP, SUPPORT_STOP,
StateVacuumEntity, StateVacuumEntity,
) )
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON
from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.util.dt import as_utc from homeassistant.util.dt import as_utc
from .const import ( from .const import (
CONF_DEVICE,
CONF_FLOW_TYPE,
DOMAIN,
SERVICE_CLEAN_SEGMENT, SERVICE_CLEAN_SEGMENT,
SERVICE_CLEAN_ZONE, SERVICE_CLEAN_ZONE,
SERVICE_GOTO, SERVICE_GOTO,
@ -39,11 +43,11 @@ from .const import (
SERVICE_START_REMOTE_CONTROL, SERVICE_START_REMOTE_CONTROL,
SERVICE_STOP_REMOTE_CONTROL, SERVICE_STOP_REMOTE_CONTROL,
) )
from .device import XiaomiMiioEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Xiaomi Vacuum cleaner" DEFAULT_NAME = "Xiaomi Vacuum cleaner"
DATA_KEY = "vacuum.xiaomi_miio"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{ {
@ -116,110 +120,124 @@ STATE_CODE_TO_STATE = {
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 Xiaomi vacuum cleaner robot platform.""" """Import Miio configuration from YAML."""
if DATA_KEY not in hass.data: _LOGGER.warning(
hass.data[DATA_KEY] = {} "Loading Xiaomi Miio Vacuum via platform setup is deprecated. Please remove it from your configuration."
host = config[CONF_HOST]
token = config[CONF_TOKEN]
name = config[CONF_NAME]
# Create handler
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
vacuum = Vacuum(host, token)
mirobo = MiroboVacuum(name, vacuum)
hass.data[DATA_KEY][host] = mirobo
async_add_entities([mirobo], update_before_add=True)
platform = entity_platform.current_platform.get()
platform.async_register_entity_service(
SERVICE_START_REMOTE_CONTROL,
{},
MiroboVacuum.async_remote_control_start.__name__,
) )
hass.async_create_task(
platform.async_register_entity_service( hass.config_entries.flow.async_init(
SERVICE_STOP_REMOTE_CONTROL, DOMAIN,
{}, context={"source": SOURCE_IMPORT},
MiroboVacuum.async_remote_control_stop.__name__, data=config,
) )
platform.async_register_entity_service(
SERVICE_MOVE_REMOTE_CONTROL,
{
vol.Optional(ATTR_RC_VELOCITY): vol.All(
vol.Coerce(float), vol.Clamp(min=-0.29, max=0.29)
),
vol.Optional(ATTR_RC_ROTATION): vol.All(
vol.Coerce(int), vol.Clamp(min=-179, max=179)
),
vol.Optional(ATTR_RC_DURATION): cv.positive_int,
},
MiroboVacuum.async_remote_control_move.__name__,
)
platform.async_register_entity_service(
SERVICE_MOVE_REMOTE_CONTROL_STEP,
{
vol.Optional(ATTR_RC_VELOCITY): vol.All(
vol.Coerce(float), vol.Clamp(min=-0.29, max=0.29)
),
vol.Optional(ATTR_RC_ROTATION): vol.All(
vol.Coerce(int), vol.Clamp(min=-179, max=179)
),
vol.Optional(ATTR_RC_DURATION): cv.positive_int,
},
MiroboVacuum.async_remote_control_move_step.__name__,
)
platform.async_register_entity_service(
SERVICE_CLEAN_ZONE,
{
vol.Required(ATTR_ZONE_ARRAY): vol.All(
list,
[
vol.ExactSequence(
[
vol.Coerce(int),
vol.Coerce(int),
vol.Coerce(int),
vol.Coerce(int),
]
)
],
),
vol.Required(ATTR_ZONE_REPEATER): vol.All(
vol.Coerce(int), vol.Clamp(min=1, max=3)
),
},
MiroboVacuum.async_clean_zone.__name__,
)
platform.async_register_entity_service(
SERVICE_GOTO,
{
vol.Required("x_coord"): vol.Coerce(int),
vol.Required("y_coord"): vol.Coerce(int),
},
MiroboVacuum.async_goto.__name__,
)
platform.async_register_entity_service(
SERVICE_CLEAN_SEGMENT,
{vol.Required("segments"): vol.Any(vol.Coerce(int), [vol.Coerce(int)])},
MiroboVacuum.async_clean_segment.__name__,
) )
class MiroboVacuum(StateVacuumEntity): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Xiaomi vacuum cleaner robot from a config entry."""
entities = []
if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
host = config_entry.data[CONF_HOST]
token = config_entry.data[CONF_TOKEN]
name = config_entry.title
unique_id = config_entry.unique_id
# Create handler
_LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5])
vacuum = Vacuum(host, token)
mirobo = MiroboVacuum(name, vacuum, config_entry, unique_id)
entities.append(mirobo)
platform = entity_platform.current_platform.get()
platform.async_register_entity_service(
SERVICE_START_REMOTE_CONTROL,
{},
MiroboVacuum.async_remote_control_start.__name__,
)
platform.async_register_entity_service(
SERVICE_STOP_REMOTE_CONTROL,
{},
MiroboVacuum.async_remote_control_stop.__name__,
)
platform.async_register_entity_service(
SERVICE_MOVE_REMOTE_CONTROL,
{
vol.Optional(ATTR_RC_VELOCITY): vol.All(
vol.Coerce(float), vol.Clamp(min=-0.29, max=0.29)
),
vol.Optional(ATTR_RC_ROTATION): vol.All(
vol.Coerce(int), vol.Clamp(min=-179, max=179)
),
vol.Optional(ATTR_RC_DURATION): cv.positive_int,
},
MiroboVacuum.async_remote_control_move.__name__,
)
platform.async_register_entity_service(
SERVICE_MOVE_REMOTE_CONTROL_STEP,
{
vol.Optional(ATTR_RC_VELOCITY): vol.All(
vol.Coerce(float), vol.Clamp(min=-0.29, max=0.29)
),
vol.Optional(ATTR_RC_ROTATION): vol.All(
vol.Coerce(int), vol.Clamp(min=-179, max=179)
),
vol.Optional(ATTR_RC_DURATION): cv.positive_int,
},
MiroboVacuum.async_remote_control_move_step.__name__,
)
platform.async_register_entity_service(
SERVICE_CLEAN_ZONE,
{
vol.Required(ATTR_ZONE_ARRAY): vol.All(
list,
[
vol.ExactSequence(
[
vol.Coerce(int),
vol.Coerce(int),
vol.Coerce(int),
vol.Coerce(int),
]
)
],
),
vol.Required(ATTR_ZONE_REPEATER): vol.All(
vol.Coerce(int), vol.Clamp(min=1, max=3)
),
},
MiroboVacuum.async_clean_zone.__name__,
)
platform.async_register_entity_service(
SERVICE_GOTO,
{
vol.Required("x_coord"): vol.Coerce(int),
vol.Required("y_coord"): vol.Coerce(int),
},
MiroboVacuum.async_goto.__name__,
)
platform.async_register_entity_service(
SERVICE_CLEAN_SEGMENT,
{vol.Required("segments"): vol.Any(vol.Coerce(int), [vol.Coerce(int)])},
MiroboVacuum.async_clean_segment.__name__,
)
async_add_entities(entities, update_before_add=True)
class MiroboVacuum(XiaomiMiioEntity, StateVacuumEntity):
"""Representation of a Xiaomi Vacuum cleaner robot.""" """Representation of a Xiaomi Vacuum cleaner robot."""
def __init__(self, name, vacuum): def __init__(self, name, device, entry, unique_id):
"""Initialize the Xiaomi vacuum cleaner robot handler.""" """Initialize the Xiaomi vacuum cleaner robot handler."""
self._name = name super().__init__(name, device, entry, unique_id)
self._vacuum = vacuum
self.vacuum_state = None self.vacuum_state = None
self._available = False self._available = False
@ -233,11 +251,6 @@ class MiroboVacuum(StateVacuumEntity):
self._timers = None self._timers = None
@property
def name(self):
"""Return the name of the device."""
return self._name
@property @property
def state(self): def state(self):
"""Return the status of the vacuum cleaner.""" """Return the status of the vacuum cleaner."""
@ -364,16 +377,16 @@ class MiroboVacuum(StateVacuumEntity):
async def async_start(self): async def async_start(self):
"""Start or resume the cleaning task.""" """Start or resume the cleaning task."""
await self._try_command( await self._try_command(
"Unable to start the vacuum: %s", self._vacuum.resume_or_start "Unable to start the vacuum: %s", self._device.resume_or_start
) )
async def async_pause(self): async def async_pause(self):
"""Pause the cleaning task.""" """Pause the cleaning task."""
await self._try_command("Unable to set start/pause: %s", self._vacuum.pause) await self._try_command("Unable to set start/pause: %s", self._device.pause)
async def async_stop(self, **kwargs): async def async_stop(self, **kwargs):
"""Stop the vacuum cleaner.""" """Stop the vacuum cleaner."""
await self._try_command("Unable to stop: %s", self._vacuum.stop) await self._try_command("Unable to stop: %s", self._device.stop)
async def async_set_fan_speed(self, fan_speed, **kwargs): async def async_set_fan_speed(self, fan_speed, **kwargs):
"""Set fan speed.""" """Set fan speed."""
@ -390,28 +403,28 @@ class MiroboVacuum(StateVacuumEntity):
) )
return return
await self._try_command( await self._try_command(
"Unable to set fan speed: %s", self._vacuum.set_fan_speed, fan_speed "Unable to set fan speed: %s", self._device.set_fan_speed, fan_speed
) )
async def async_return_to_base(self, **kwargs): async def async_return_to_base(self, **kwargs):
"""Set the vacuum cleaner to return to the dock.""" """Set the vacuum cleaner to return to the dock."""
await self._try_command("Unable to return home: %s", self._vacuum.home) await self._try_command("Unable to return home: %s", self._device.home)
async def async_clean_spot(self, **kwargs): async def async_clean_spot(self, **kwargs):
"""Perform a spot clean-up.""" """Perform a spot clean-up."""
await self._try_command( await self._try_command(
"Unable to start the vacuum for a spot clean-up: %s", self._vacuum.spot "Unable to start the vacuum for a spot clean-up: %s", self._device.spot
) )
async def async_locate(self, **kwargs): async def async_locate(self, **kwargs):
"""Locate the vacuum cleaner.""" """Locate the vacuum cleaner."""
await self._try_command("Unable to locate the botvac: %s", self._vacuum.find) await self._try_command("Unable to locate the botvac: %s", self._device.find)
async def async_send_command(self, command, params=None, **kwargs): async def async_send_command(self, command, params=None, **kwargs):
"""Send raw command.""" """Send raw command."""
await self._try_command( await self._try_command(
"Unable to send command to the vacuum: %s", "Unable to send command to the vacuum: %s",
self._vacuum.raw_command, self._device.raw_command,
command, command,
params, params,
) )
@ -419,13 +432,13 @@ class MiroboVacuum(StateVacuumEntity):
async def async_remote_control_start(self): async def async_remote_control_start(self):
"""Start remote control mode.""" """Start remote control mode."""
await self._try_command( await self._try_command(
"Unable to start remote control the vacuum: %s", self._vacuum.manual_start "Unable to start remote control the vacuum: %s", self._device.manual_start
) )
async def async_remote_control_stop(self): async def async_remote_control_stop(self):
"""Stop remote control mode.""" """Stop remote control mode."""
await self._try_command( await self._try_command(
"Unable to stop remote control the vacuum: %s", self._vacuum.manual_stop "Unable to stop remote control the vacuum: %s", self._device.manual_stop
) )
async def async_remote_control_move( async def async_remote_control_move(
@ -434,7 +447,7 @@ class MiroboVacuum(StateVacuumEntity):
"""Move vacuum with remote control mode.""" """Move vacuum with remote control mode."""
await self._try_command( await self._try_command(
"Unable to move with remote control the vacuum: %s", "Unable to move with remote control the vacuum: %s",
self._vacuum.manual_control, self._device.manual_control,
velocity=velocity, velocity=velocity,
rotation=rotation, rotation=rotation,
duration=duration, duration=duration,
@ -446,7 +459,7 @@ class MiroboVacuum(StateVacuumEntity):
"""Move vacuum one step with remote control mode.""" """Move vacuum one step with remote control mode."""
await self._try_command( await self._try_command(
"Unable to remote control the vacuum: %s", "Unable to remote control the vacuum: %s",
self._vacuum.manual_control_once, self._device.manual_control_once,
velocity=velocity, velocity=velocity,
rotation=rotation, rotation=rotation,
duration=duration, duration=duration,
@ -456,7 +469,7 @@ class MiroboVacuum(StateVacuumEntity):
"""Goto the specified coordinates.""" """Goto the specified coordinates."""
await self._try_command( await self._try_command(
"Unable to send the vacuum cleaner to the specified coordinates: %s", "Unable to send the vacuum cleaner to the specified coordinates: %s",
self._vacuum.goto, self._device.goto,
x_coord=x_coord, x_coord=x_coord,
y_coord=y_coord, y_coord=y_coord,
) )
@ -468,23 +481,23 @@ class MiroboVacuum(StateVacuumEntity):
await self._try_command( await self._try_command(
"Unable to start cleaning of the specified segments: %s", "Unable to start cleaning of the specified segments: %s",
self._vacuum.segment_clean, self._device.segment_clean,
segments=segments, segments=segments,
) )
def update(self): def update(self):
"""Fetch state from the device.""" """Fetch state from the device."""
try: try:
state = self._vacuum.status() state = self._device.status()
self.vacuum_state = state self.vacuum_state = state
self._fan_speeds = self._vacuum.fan_speed_presets() self._fan_speeds = self._device.fan_speed_presets()
self._fan_speeds_reverse = {v: k for k, v in self._fan_speeds.items()} self._fan_speeds_reverse = {v: k for k, v in self._fan_speeds.items()}
self.consumable_state = self._vacuum.consumable_status() self.consumable_state = self._device.consumable_status()
self.clean_history = self._vacuum.clean_history() self.clean_history = self._device.clean_history()
self.last_clean = self._vacuum.last_clean_details() self.last_clean = self._device.last_clean_details()
self.dnd_state = self._vacuum.dnd_status() self.dnd_state = self._device.dnd_status()
self._available = True self._available = True
except (OSError, DeviceException) as exc: except (OSError, DeviceException) as exc:
@ -494,7 +507,7 @@ class MiroboVacuum(StateVacuumEntity):
# Fetch timers separately, see #38285 # Fetch timers separately, see #38285
try: try:
self._timers = self._vacuum.timer() self._timers = self._device.timer()
except DeviceException as exc: except DeviceException as exc:
_LOGGER.debug( _LOGGER.debug(
"Unable to fetch timers, this may happen on some devices: %s", exc "Unable to fetch timers, this may happen on some devices: %s", exc
@ -507,6 +520,6 @@ class MiroboVacuum(StateVacuumEntity):
_zone.append(repeats) _zone.append(repeats)
_LOGGER.debug("Zone with repeats: %s", zone) _LOGGER.debug("Zone with repeats: %s", zone)
try: try:
await self.hass.async_add_executor_job(self._vacuum.zoned_clean, zone) await self.hass.async_add_executor_job(self._device.zoned_clean, zone)
except (OSError, DeviceException) as exc: except (OSError, DeviceException) as exc:
_LOGGER.error("Unable to send zoned_clean command to the vacuum: %s", exc) _LOGGER.error("Unable to send zoned_clean command to the vacuum: %s", exc)

View File

@ -257,6 +257,53 @@ async def test_import_flow_success(hass):
} }
async def test_config_flow_step_device_manual_model_succes(hass):
"""Test config flow, device connection error, manual model."""
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"}
overwrite_model = const.MODELS_VACUUM[0]
with patch(
"homeassistant.components.xiaomi_miio.device.Device.info",
side_effect=DeviceException({}),
), patch(
"homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: TEST_TOKEN, const.CONF_MODEL: overwrite_model},
)
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: overwrite_model,
const.CONF_MAC: None,
}
async def config_flow_device_success(hass, model_to_test): async def config_flow_device_success(hass, model_to_test):
"""Test a successful config flow for a device (base class).""" """Test a successful config flow for a device (base class)."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -342,3 +389,16 @@ async def test_zeroconf_plug_success(hass):
test_plug_model = const.MODELS_SWITCH[0] test_plug_model = const.MODELS_SWITCH[0]
test_zeroconf_name = const.MODELS_SWITCH[0].replace(".", "-") test_zeroconf_name = const.MODELS_SWITCH[0].replace(".", "-")
await zeroconf_device_success(hass, test_zeroconf_name, test_plug_model) await zeroconf_device_success(hass, test_zeroconf_name, test_plug_model)
async def test_config_flow_vacuum_success(hass):
"""Test a successful config flow for a vacuum."""
test_vacuum_model = const.MODELS_VACUUM[0]
await config_flow_device_success(hass, test_vacuum_model)
async def test_zeroconf_vacuum_success(hass):
"""Test a successful zeroconf discovery of a vacuum."""
test_vacuum_model = const.MODELS_VACUUM[0]
test_zeroconf_name = const.MODELS_VACUUM[0].replace(".", "-")
await zeroconf_device_success(hass, test_zeroconf_name, test_vacuum_model)

View File

@ -22,6 +22,7 @@ from homeassistant.components.vacuum import (
STATE_CLEANING, STATE_CLEANING,
STATE_ERROR, STATE_ERROR,
) )
from homeassistant.components.xiaomi_miio import const
from homeassistant.components.xiaomi_miio.const import DOMAIN as XIAOMI_DOMAIN from homeassistant.components.xiaomi_miio.const import DOMAIN as XIAOMI_DOMAIN
from homeassistant.components.xiaomi_miio.vacuum import ( from homeassistant.components.xiaomi_miio.vacuum import (
ATTR_CLEANED_AREA, ATTR_CLEANED_AREA,
@ -38,7 +39,6 @@ from homeassistant.components.xiaomi_miio.vacuum import (
ATTR_SIDE_BRUSH_LEFT, ATTR_SIDE_BRUSH_LEFT,
ATTR_TIMERS, ATTR_TIMERS,
CONF_HOST, CONF_HOST,
CONF_NAME,
CONF_TOKEN, CONF_TOKEN,
SERVICE_CLEAN_SEGMENT, SERVICE_CLEAN_SEGMENT,
SERVICE_CLEAN_ZONE, SERVICE_CLEAN_ZONE,
@ -51,12 +51,14 @@ from homeassistant.components.xiaomi_miio.vacuum import (
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES, ATTR_SUPPORTED_FEATURES,
CONF_PLATFORM,
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
) )
from homeassistant.setup import async_setup_component
from .test_config_flow import TEST_MAC
from tests.common import MockConfigEntry
PLATFORM = "xiaomi_miio" PLATFORM = "xiaomi_miio"
@ -521,17 +523,21 @@ async def setup_component(hass, entity_name):
"""Set up vacuum component.""" """Set up vacuum component."""
entity_id = f"{DOMAIN}.{entity_name}" entity_id = f"{DOMAIN}.{entity_name}"
await async_setup_component( config_entry = MockConfigEntry(
hass, domain=XIAOMI_DOMAIN,
DOMAIN, unique_id="123456",
{ title=entity_name,
DOMAIN: { data={
CONF_PLATFORM: PLATFORM, const.CONF_FLOW_TYPE: const.CONF_DEVICE,
CONF_HOST: "192.168.1.100", CONF_HOST: "192.168.1.100",
CONF_NAME: entity_name, CONF_TOKEN: "12345678901234567890123456789012",
CONF_TOKEN: "12345678901234567890123456789012", const.CONF_MODEL: const.MODELS_VACUUM[0],
} const.CONF_MAC: TEST_MAC,
}, },
) )
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
return entity_id return entity_id