From 338c07a56b3a9064b2a44a4834d969e2b1865209 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 22 Feb 2021 13:01:02 +0100 Subject: [PATCH] Add Xiaomi Miio vacuum config flow (#46669) --- .../components/xiaomi_miio/__init__.py | 9 +- .../components/xiaomi_miio/config_flow.py | 68 +++-- homeassistant/components/xiaomi_miio/const.py | 4 + .../components/xiaomi_miio/device.py | 8 +- .../components/xiaomi_miio/strings.json | 2 +- .../xiaomi_miio/translations/en.json | 21 +- .../components/xiaomi_miio/vacuum.py | 263 +++++++++--------- .../xiaomi_miio/test_config_flow.py | 60 ++++ tests/components/xiaomi_miio/test_vacuum.py | 32 ++- 9 files changed, 278 insertions(+), 189 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 273fc53da5a..a8b32a31576 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -17,6 +17,7 @@ from .const import ( DOMAIN, KEY_COORDINATOR, MODELS_SWITCH, + MODELS_VACUUM, ) from .gateway import ConnectXiaomiGateway @@ -24,6 +25,7 @@ _LOGGER = logging.getLogger(__name__) GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor", "light"] SWITCH_PLATFORMS = ["switch"] +VACUUM_PLATFORMS = ["vacuum"] async def async_setup(hass: core.HomeAssistant, config: dict): @@ -117,9 +119,14 @@ async def async_setup_device_entry( model = entry.data[CONF_MODEL] # Identify platforms to setup + platforms = [] if model in MODELS_SWITCH: platforms = SWITCH_PLATFORMS - else: + for vacuum_model in MODELS_VACUUM: + if model.startswith(vacuum_model): + platforms = VACUUM_PLATFORMS + + if not platforms: return False for component in platforms: diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 2a1532eaf9b..d7e2198f72f 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -15,8 +15,9 @@ from .const import ( CONF_MAC, CONF_MODEL, DOMAIN, + MODELS_ALL, + MODELS_ALL_DEVICES, MODELS_GATEWAY, - MODELS_SWITCH, ) from .device import ConnectXiaomiDevice @@ -29,6 +30,7 @@ DEVICE_SETTINGS = { vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)), } DEVICE_CONFIG = vol.Schema({vol.Required(CONF_HOST): str}).extend(DEVICE_SETTINGS) +DEVICE_MODEL_CONFIG = {vol.Optional(CONF_MODEL): vol.In(MODELS_ALL)} class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -40,6 +42,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize.""" self.host = None + self.mac = None async def async_step_import(self, conf: dict): """Import a configuration from config.yaml.""" @@ -53,15 +56,15 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle zeroconf discovery.""" name = discovery_info.get("name") 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") # Check which device is discovered. for gateway_model in MODELS_GATEWAY: 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) 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() - for switch_model in MODELS_SWITCH: - if name.startswith(switch_model.replace(".", "-")): - unique_id = format_mac(mac_address) + for device_model in MODELS_ALL_DEVICES: + if name.startswith(device_model.replace(".", "-")): + unique_id = format_mac(self.mac) await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured({CONF_HOST: self.host}) @@ -95,6 +98,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: token = user_input[CONF_TOKEN] + model = user_input.get(CONF_MODEL) if user_input.get(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) 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 for gateway_model in MODELS_GATEWAY: - if device_info.model.startswith(gateway_model): - mac = format_mac(device_info.mac_address) - unique_id = mac + if model.startswith(gateway_model): + unique_id = self.mac await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() return self.async_create_entry( @@ -117,29 +126,29 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_FLOW_TYPE: CONF_GATEWAY, CONF_HOST: self.host, CONF_TOKEN: token, - CONF_MODEL: device_info.model, - CONF_MAC: mac, + CONF_MODEL: model, + CONF_MAC: self.mac, }, ) # 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, - }, - ) + for device_model in MODELS_ALL_DEVICES: + if model.startswith(device_model): + unique_id = self.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: model, + CONF_MAC: self.mac, + }, + ) errors["base"] = "unknown_device" else: errors["base"] = "cannot_connect" @@ -149,4 +158,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): else: schema = DEVICE_CONFIG + if errors: + schema = schema.extend(DEVICE_MODEL_CONFIG) + return self.async_show_form(step_id="device", data_schema=schema, errors=errors) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index c0ddb698340..d6c39146f6a 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -23,6 +23,10 @@ MODELS_SWITCH = [ "chuangmi.plug.hmi206", "lumi.acpartner.v3", ] +MODELS_VACUUM = ["roborock.vacuum"] + +MODELS_ALL_DEVICES = MODELS_SWITCH + MODELS_VACUUM +MODELS_ALL = MODELS_ALL_DEVICES + MODELS_GATEWAY # Fan Services SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on" diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index 48bedbf0cc8..cb91726ecad 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -78,10 +78,14 @@ class XiaomiMiioEntity(Entity): @property def device_info(self): """Return the device info.""" - return { - "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac)}, + device_info = { "identifiers": {(DOMAIN, self._device_id)}, "manufacturer": "Xiaomi", "name": self._name, "model": self._model, } + + if self._mac is not None: + device_info["connections"] = {(dr.CONNECTION_NETWORK_MAC, self._mac)} + + return device_info diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index 1ab0c6f51c6..90710baebca 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -8,7 +8,7 @@ "data": { "host": "[%key:common::config_flow::data::ip%]", "token": "[%key:common::config_flow::data::api_token%]", - "name": "Name of the device" + "model": "Device model (Optional)" } } }, diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json index fe95af5e06c..37a8ce06eba 100644 --- a/homeassistant/components/xiaomi_miio/translations/en.json +++ b/homeassistant/components/xiaomi_miio/translations/en.json @@ -6,7 +6,6 @@ }, "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}", @@ -14,27 +13,11 @@ "device": { "data": { "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.", "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" } } } diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index ab76d14a69a..7bdbfca7bc9 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -26,11 +26,15 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, StateVacuumEntity, ) +from homeassistant.config_entries import SOURCE_IMPORT 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.util.dt import as_utc from .const import ( + CONF_DEVICE, + CONF_FLOW_TYPE, + DOMAIN, SERVICE_CLEAN_SEGMENT, SERVICE_CLEAN_ZONE, SERVICE_GOTO, @@ -39,11 +43,11 @@ from .const import ( SERVICE_START_REMOTE_CONTROL, SERVICE_STOP_REMOTE_CONTROL, ) +from .device import XiaomiMiioEntity _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Vacuum cleaner" -DATA_KEY = "vacuum.xiaomi_miio" 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): - """Set up the Xiaomi vacuum cleaner robot platform.""" - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} - - 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__, + """Import Miio configuration from YAML.""" + _LOGGER.warning( + "Loading Xiaomi Miio Vacuum via platform setup is deprecated. Please remove it from your configuration." ) - - 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__, + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) ) -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.""" - def __init__(self, name, vacuum): + def __init__(self, name, device, entry, unique_id): """Initialize the Xiaomi vacuum cleaner robot handler.""" - self._name = name - self._vacuum = vacuum + super().__init__(name, device, entry, unique_id) self.vacuum_state = None self._available = False @@ -233,11 +251,6 @@ class MiroboVacuum(StateVacuumEntity): self._timers = None - @property - def name(self): - """Return the name of the device.""" - return self._name - @property def state(self): """Return the status of the vacuum cleaner.""" @@ -364,16 +377,16 @@ class MiroboVacuum(StateVacuumEntity): async def async_start(self): """Start or resume the cleaning task.""" 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): """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): """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): """Set fan speed.""" @@ -390,28 +403,28 @@ class MiroboVacuum(StateVacuumEntity): ) return 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): """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): """Perform a spot clean-up.""" 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): """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): """Send raw command.""" await self._try_command( "Unable to send command to the vacuum: %s", - self._vacuum.raw_command, + self._device.raw_command, command, params, ) @@ -419,13 +432,13 @@ class MiroboVacuum(StateVacuumEntity): async def async_remote_control_start(self): """Start remote control mode.""" 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): """Stop remote control mode.""" 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( @@ -434,7 +447,7 @@ class MiroboVacuum(StateVacuumEntity): """Move vacuum with remote control mode.""" await self._try_command( "Unable to move with remote control the vacuum: %s", - self._vacuum.manual_control, + self._device.manual_control, velocity=velocity, rotation=rotation, duration=duration, @@ -446,7 +459,7 @@ class MiroboVacuum(StateVacuumEntity): """Move vacuum one step with remote control mode.""" await self._try_command( "Unable to remote control the vacuum: %s", - self._vacuum.manual_control_once, + self._device.manual_control_once, velocity=velocity, rotation=rotation, duration=duration, @@ -456,7 +469,7 @@ class MiroboVacuum(StateVacuumEntity): """Goto the specified coordinates.""" await self._try_command( "Unable to send the vacuum cleaner to the specified coordinates: %s", - self._vacuum.goto, + self._device.goto, x_coord=x_coord, y_coord=y_coord, ) @@ -468,23 +481,23 @@ class MiroboVacuum(StateVacuumEntity): await self._try_command( "Unable to start cleaning of the specified segments: %s", - self._vacuum.segment_clean, + self._device.segment_clean, segments=segments, ) def update(self): """Fetch state from the device.""" try: - state = self._vacuum.status() + state = self._device.status() 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.consumable_state = self._vacuum.consumable_status() - self.clean_history = self._vacuum.clean_history() - self.last_clean = self._vacuum.last_clean_details() - self.dnd_state = self._vacuum.dnd_status() + self.consumable_state = self._device.consumable_status() + self.clean_history = self._device.clean_history() + self.last_clean = self._device.last_clean_details() + self.dnd_state = self._device.dnd_status() self._available = True except (OSError, DeviceException) as exc: @@ -494,7 +507,7 @@ class MiroboVacuum(StateVacuumEntity): # Fetch timers separately, see #38285 try: - self._timers = self._vacuum.timer() + self._timers = self._device.timer() except DeviceException as exc: _LOGGER.debug( "Unable to fetch timers, this may happen on some devices: %s", exc @@ -507,6 +520,6 @@ class MiroboVacuum(StateVacuumEntity): _zone.append(repeats) _LOGGER.debug("Zone with repeats: %s", zone) 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: _LOGGER.error("Unable to send zoned_clean command to the vacuum: %s", exc) diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 220c51034f1..f4f7b5e2b46 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -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): """Test a successful config flow for a device (base class).""" 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_zeroconf_name = const.MODELS_SWITCH[0].replace(".", "-") 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) diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index b1a3c08b84b..23e5d8884b3 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -22,6 +22,7 @@ from homeassistant.components.vacuum import ( STATE_CLEANING, 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.vacuum import ( ATTR_CLEANED_AREA, @@ -38,7 +39,6 @@ from homeassistant.components.xiaomi_miio.vacuum import ( ATTR_SIDE_BRUSH_LEFT, ATTR_TIMERS, CONF_HOST, - CONF_NAME, CONF_TOKEN, SERVICE_CLEAN_SEGMENT, SERVICE_CLEAN_ZONE, @@ -51,12 +51,14 @@ from homeassistant.components.xiaomi_miio.vacuum import ( from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, - CONF_PLATFORM, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.setup import async_setup_component + +from .test_config_flow import TEST_MAC + +from tests.common import MockConfigEntry PLATFORM = "xiaomi_miio" @@ -521,17 +523,21 @@ async def setup_component(hass, entity_name): """Set up vacuum component.""" entity_id = f"{DOMAIN}.{entity_name}" - await async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - CONF_PLATFORM: PLATFORM, - CONF_HOST: "192.168.1.100", - CONF_NAME: entity_name, - CONF_TOKEN: "12345678901234567890123456789012", - } + config_entry = MockConfigEntry( + domain=XIAOMI_DOMAIN, + unique_id="123456", + title=entity_name, + data={ + const.CONF_FLOW_TYPE: const.CONF_DEVICE, + CONF_HOST: "192.168.1.100", + 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() + return entity_id