From b6c01ba4ee663e4edfdf50e9ed8e97dde66f80e6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 16 Feb 2021 23:11:48 +0100 Subject: [PATCH 01/18] Miio Vacuum Config Flow --- .../components/xiaomi_miio/__init__.py | 9 +- .../components/xiaomi_miio/config_flow.py | 8 + homeassistant/components/xiaomi_miio/const.py | 1 + .../components/xiaomi_miio/vacuum.py | 245 ++++++++++-------- 4 files changed, 147 insertions(+), 116 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 273fc53da5ae6f..a8b32a31576ee5 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -17,6 +17,7 @@ DOMAIN, KEY_COORDINATOR, MODELS_SWITCH, + MODELS_VACUUM, ) from .gateway import ConnectXiaomiGateway @@ -24,6 +25,7 @@ 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 2a1532eaf9b813..b71e249a9daa8f 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -17,6 +17,7 @@ DOMAIN, MODELS_GATEWAY, MODELS_SWITCH, + MODELS_VACUUM, ) from .device import ConnectXiaomiDevice @@ -125,7 +126,14 @@ async def async_step_device(self, user_input=None): # Setup all other Miio Devices name = user_input.get(CONF_NAME, DEFAULT_DEVICE_NAME) + known_device = False if device_info.model in MODELS_SWITCH: + known_device = True + for vacuum_model in MODELS_VACUUM: + if device_info.model.startswith(vacuum_model): + known_device = True + + if known_device: mac = format_mac(device_info.mac_address) unique_id = mac await self.async_set_unique_id(unique_id) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index c0ddb6983409b3..b5a7a3dbc9f449 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -23,6 +23,7 @@ "chuangmi.plug.hmi206", "lumi.acpartner.v3", ] +MODELS_VACUUM = ["roborock.vacuum"] # Fan Services SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on" diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index ab76d14a69ae44..1b90dafbababa2 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -31,6 +31,8 @@ from homeassistant.util.dt import as_utc from .const import ( + CONF_DEVICE, + CONF_FLOW_TYPE, SERVICE_CLEAN_SEGMENT, SERVICE_CLEAN_ZONE, SERVICE_GOTO, @@ -39,6 +41,7 @@ SERVICE_START_REMOTE_CONTROL, SERVICE_STOP_REMOTE_CONTROL, ) +from .device import XiaomiMiioEntity _LOGGER = logging.getLogger(__name__) @@ -116,110 +119,127 @@ 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] = {} + """Import Miio configuration from YAML.""" + _LOGGER.warning( + "Loading Xiaomi Miio Vacuum 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] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Xiaomi vacuum cleaner robot from a config entry.""" + entities = [] - # Create handler - _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) - vacuum = Vacuum(host, token) + if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} - mirobo = MiroboVacuum(name, vacuum) - hass.data[DATA_KEY][host] = mirobo + host = config_entry.data[CONF_HOST] + token = config_entry.data[CONF_TOKEN] + name = config_entry.title + unique_id = config_entry.unique_id - async_add_entities([mirobo], update_before_add=True) + # Create handler + _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) + vacuum = Vacuum(host, token) - platform = entity_platform.current_platform.get() + mirobo = MiroboVacuum(name, vacuum, config_entry, unique_id) + hass.data[DATA_KEY][host] = mirobo + entities.append(mirobo) - platform.async_register_entity_service( - SERVICE_START_REMOTE_CONTROL, - {}, - MiroboVacuum.async_remote_control_start.__name__, - ) + platform = entity_platform.current_platform.get() - platform.async_register_entity_service( - SERVICE_STOP_REMOTE_CONTROL, - {}, - MiroboVacuum.async_remote_control_stop.__name__, - ) + platform.async_register_entity_service( + SERVICE_START_REMOTE_CONTROL, + {}, + MiroboVacuum.async_remote_control_start.__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_STOP_REMOTE_CONTROL, + {}, + MiroboVacuum.async_remote_control_stop.__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_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_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_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_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__, - ) + 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(StateVacuumEntity): + +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 +253,6 @@ def __init__(self, name, vacuum): 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 +379,16 @@ async def _try_command(self, mask_error, func, *args, **kwargs): 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 +405,28 @@ async def async_set_fan_speed(self, fan_speed, **kwargs): ) 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 +434,13 @@ async def async_send_command(self, command, params=None, **kwargs): 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 +449,7 @@ async def async_remote_control_move( """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 +461,7 @@ async def async_remote_control_move_step( """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 +471,7 @@ async def async_goto(self, x_coord: int, y_coord: int): """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 +483,23 @@ async def async_clean_segment(self, segments): 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 +509,7 @@ def update(self): # 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 +522,6 @@ async def async_clean_zone(self, zone, repeats=1): _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) From ccbde0b38710063acd02de5d73b19b7bd42a086c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 16 Feb 2021 23:26:40 +0100 Subject: [PATCH 02/18] fix imports --- homeassistant/components/xiaomi_miio/vacuum.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 1b90dafbababa2..6d0651defe4b20 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -26,6 +26,7 @@ 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 @@ -33,6 +34,7 @@ from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, + DOMAIN, SERVICE_CLEAN_SEGMENT, SERVICE_CLEAN_ZONE, SERVICE_GOTO, @@ -131,6 +133,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) ) + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Xiaomi vacuum cleaner robot from a config entry.""" entities = [] From 600d46d2cf4e73eb191711f7eacd6c3c9a678942 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 16 Feb 2021 23:32:54 +0100 Subject: [PATCH 03/18] add vacuum config flow tests --- tests/components/xiaomi_miio/test_config_flow.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 220c51034f1766..5e8ff8d293b5ab 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -342,3 +342,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) From 9a405e723ef9897178aaf8d5a9c48b54d23f83bd Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 16 Feb 2021 23:54:27 +0100 Subject: [PATCH 04/18] Partially fix tests --- tests/components/xiaomi_miio/test_vacuum.py | 48 +++++++++++++++------ 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index b1a3c08b84b690..00d78571012c1f 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -7,6 +7,10 @@ import pytest from pytz import utc +from homeassistant import config_entries +from homeassistant.components.xiaomi_miio.config_flow import ( + DEFAULT_DEVICE_NAME, +) from homeassistant.components.vacuum import ( ATTR_BATTERY_ICON, ATTR_FAN_SPEED, @@ -22,6 +26,7 @@ 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, @@ -58,6 +63,8 @@ ) from homeassistant.setup import async_setup_component +from .test_config_flow import get_mock_info, TEST_MAC + PLATFORM = "xiaomi_miio" # calls made when device status is requested @@ -519,19 +526,36 @@ async def test_xiaomi_vacuum_clean_segment_service_single_segment( async def setup_component(hass, entity_name): """Set up vacuum component.""" - entity_id = f"{DOMAIN}.{entity_name}" + entity_id = f"{DOMAIN}.xiaomi_device" - await async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - CONF_PLATFORM: PLATFORM, - CONF_HOST: "192.168.1.100", - CONF_NAME: entity_name, - CONF_TOKEN: "12345678901234567890123456789012", - } - }, + 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=const.MODELS_VACUUM[0]) + + 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: "192.168.1.100", CONF_TOKEN: "12345678901234567890123456789012"}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == DEFAULT_DEVICE_NAME + assert result["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, + } + await hass.async_block_till_done() return entity_id From 5aaaf540da44e42c9081288dda0d238ae9339686 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 17 Feb 2021 09:54:51 +0100 Subject: [PATCH 05/18] fix tests --- .../components/xiaomi_miio/config_flow.py | 2 +- tests/components/xiaomi_miio/test_vacuum.py | 55 +++++++------------ 2 files changed, 20 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index b71e249a9daa8f..423eaed2fcecd3 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -71,7 +71,7 @@ async def async_step_zeroconf(self, discovery_info): ) return await self.async_step_device() - for switch_model in MODELS_SWITCH: + for switch_model in MODELS_SWITCH + MODELS_VACUUM: if name.startswith(switch_model.replace(".", "-")): unique_id = format_mac(mac_address) await self.async_set_unique_id(unique_id) diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 00d78571012c1f..62a2dff0ffff7e 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -8,9 +8,6 @@ from pytz import utc from homeassistant import config_entries -from homeassistant.components.xiaomi_miio.config_flow import ( - DEFAULT_DEVICE_NAME, -) from homeassistant.components.vacuum import ( ATTR_BATTERY_ICON, ATTR_FAN_SPEED, @@ -43,7 +40,6 @@ ATTR_SIDE_BRUSH_LEFT, ATTR_TIMERS, CONF_HOST, - CONF_NAME, CONF_TOKEN, SERVICE_CLEAN_SEGMENT, SERVICE_CLEAN_ZONE, @@ -56,14 +52,12 @@ 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 get_mock_info, TEST_MAC +from .test_config_flow import TEST_MAC PLATFORM = "xiaomi_miio" @@ -526,36 +520,25 @@ async def test_xiaomi_vacuum_clean_segment_service_single_segment( async def setup_component(hass, entity_name): """Set up vacuum component.""" - entity_id = f"{DOMAIN}.xiaomi_device" + entity_id = f"{DOMAIN}.{entity_name}" - result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": config_entries.SOURCE_USER} + hass.config.components.add(XIAOMI_DOMAIN) + config_entry = config_entries.ConfigEntry( + 1, + XIAOMI_DOMAIN, + entity_name, + { + 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, + }, + "test", + config_entries.CONN_CLASS_LOCAL_POLL, + system_options={}, ) - - assert result["type"] == "form" - assert result["step_id"] == "device" - assert result["errors"] == {} - - mock_info = get_mock_info(model=const.MODELS_VACUUM[0]) - - 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: "192.168.1.100", CONF_TOKEN: "12345678901234567890123456789012"}, - ) - - assert result["type"] == "create_entry" - assert result["title"] == DEFAULT_DEVICE_NAME - assert result["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, - } - + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + # To flush out the service call to update the group await hass.async_block_till_done() return entity_id From 7a73ba198bab519d9f4a227c8569001863525f76 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 17 Feb 2021 10:46:34 +0100 Subject: [PATCH 06/18] use hass.config_entries.async_setup --- tests/components/xiaomi_miio/test_vacuum.py | 23 ++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 62a2dff0ffff7e..23e5d8884b34e0 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -7,7 +7,6 @@ import pytest from pytz import utc -from homeassistant import config_entries from homeassistant.components.vacuum import ( ATTR_BATTERY_ICON, ATTR_FAN_SPEED, @@ -59,6 +58,8 @@ from .test_config_flow import TEST_MAC +from tests.common import MockConfigEntry + PLATFORM = "xiaomi_miio" # calls made when device status is requested @@ -522,23 +523,21 @@ async def setup_component(hass, entity_name): """Set up vacuum component.""" entity_id = f"{DOMAIN}.{entity_name}" - hass.config.components.add(XIAOMI_DOMAIN) - config_entry = config_entries.ConfigEntry( - 1, - XIAOMI_DOMAIN, - entity_name, - { + 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, }, - "test", - config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, ) - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) - # To flush out the service call to update the group + + 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 From 0eea4be3052c6c9cb80fbd46559dd832164c553b Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 17 Feb 2021 11:00:42 +0100 Subject: [PATCH 07/18] Simplifications --- .../components/xiaomi_miio/config_flow.py | 45 ++++++++----------- homeassistant/components/xiaomi_miio/const.py | 3 ++ 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 423eaed2fcecd3..477e4666ec1a53 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -15,9 +15,8 @@ CONF_MAC, CONF_MODEL, DOMAIN, + MODELS_ALL_DEVICES, MODELS_GATEWAY, - MODELS_SWITCH, - MODELS_VACUUM, ) from .device import ConnectXiaomiDevice @@ -71,8 +70,8 @@ async def async_step_zeroconf(self, discovery_info): ) return await self.async_step_device() - for switch_model in MODELS_SWITCH + MODELS_VACUUM: - if name.startswith(switch_model.replace(".", "-")): + for device_model in MODELS_ALL_DEVICES: + if name.startswith(device_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}) @@ -126,28 +125,22 @@ async def async_step_device(self, user_input=None): # Setup all other Miio Devices name = user_input.get(CONF_NAME, DEFAULT_DEVICE_NAME) - known_device = False - if device_info.model in MODELS_SWITCH: - known_device = True - for vacuum_model in MODELS_VACUUM: - if device_info.model.startswith(vacuum_model): - known_device = True - - if known_device: - 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 device_info.model.startswith(device_model): + mac = format_mac(device_info.mac_address) + unique_id = mac + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=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" diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index b5a7a3dbc9f449..d6c39146f6ad0f 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -25,6 +25,9 @@ ] 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" SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off" From 9256c9610b0a729dc884bf6c0b1d575a0b5d79cc Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 18 Feb 2021 10:32:02 +0100 Subject: [PATCH 08/18] Add backup when info call fails --- .../components/xiaomi_miio/config_flow.py | 48 ++++++++++++++++--- .../components/xiaomi_miio/strings.json | 2 +- .../xiaomi_miio/translations/en.json | 21 +------- 3 files changed, 45 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 477e4666ec1a53..01ca3a324276f9 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +from getmac import get_mac_address from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN @@ -15,6 +16,7 @@ CONF_MAC, CONF_MODEL, DOMAIN, + MODELS_ALL, MODELS_ALL_DEVICES, MODELS_GATEWAY, ) @@ -29,7 +31,9 @@ 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): """Handle a Xiaomi Miio config flow.""" @@ -95,6 +99,7 @@ async def async_step_device(self, user_input=None): 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,10 +108,16 @@ async def async_step_device(self, user_input=None): 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: + mac = await self.async_get_mac(device_info, self.host) + + if model is not None and mac is not None: # Setup Gateways 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 = mac await self.async_set_unique_id(unique_id) @@ -117,7 +128,7 @@ async def async_step_device(self, user_input=None): CONF_FLOW_TYPE: CONF_GATEWAY, CONF_HOST: self.host, CONF_TOKEN: token, - CONF_MODEL: device_info.model, + CONF_MODEL: model, CONF_MAC: mac, }, ) @@ -126,7 +137,7 @@ async def async_step_device(self, user_input=None): name = user_input.get(CONF_NAME, DEFAULT_DEVICE_NAME) for device_model in MODELS_ALL_DEVICES: - if device_info.model.startswith(device_model): + if model.startswith(device_model): mac = format_mac(device_info.mac_address) unique_id = mac await self.async_set_unique_id(unique_id) @@ -137,7 +148,7 @@ async def async_step_device(self, user_input=None): CONF_FLOW_TYPE: CONF_DEVICE, CONF_HOST: self.host, CONF_TOKEN: token, - CONF_MODEL: device_info.model, + CONF_MODEL: model, CONF_MAC: mac, }, ) @@ -150,4 +161,29 @@ async def async_step_device(self, user_input=None): else: schema = DEVICE_CONFIG + if errors: + schema.extend(DEVICE_MODEL_CONFIG) + return self.async_show_form(step_id="device", data_schema=schema, errors=errors) + + async def async_get_mac(self, device_info, host): + """Get the mac address of a Miio Device when the info call fails.""" + if device_info is not None: + return format_mac(device_info.mac_address) + + # If the info call failed, use getmac as backup + try: + mac = await self.hass.async_add_executor_job( + partial(get_mac_address, **{"ip": host}) + ) + if not mac: + mac = await self.hass.async_add_executor_job( + partial(get_mac_address, **{"hostname": host}) + ) + except Exception as err: # pylint: disable=broad-except + _LOGGER.error("Unable to get mac address: %s", err) + mac = None + + if mac is not None: + mac = format_mac(mac) + return mac diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index 1ab0c6f51c635d..90710baebca73d 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 fe95af5e06cb0a..37a8ce06eba918 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" } } } From 8b29dca262406d6cbfc635d7e15af628e53ac1f3 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 18 Feb 2021 10:39:03 +0100 Subject: [PATCH 09/18] fix styling --- homeassistant/components/xiaomi_miio/config_flow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 01ca3a324276f9..d392d23c73b42a 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -1,8 +1,9 @@ """Config flow to configure Xiaomi Miio.""" +from functools import partial import logging -import voluptuous as vol from getmac import get_mac_address +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN @@ -31,9 +32,8 @@ vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)), } DEVICE_CONFIG = vol.Schema({vol.Required(CONF_HOST): str}).extend(DEVICE_SETTINGS) -DEVICE_MODEL_CONFIG = { - vol.Optional(CONF_MODEL): vol.In(MODELS_ALL), -} +DEVICE_MODEL_CONFIG = {vol.Optional(CONF_MODEL): vol.In(MODELS_ALL)} + class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Xiaomi Miio config flow.""" From 854ce5386e485426cdb42e5c3b316c34901a5841 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 18 Feb 2021 11:29:50 +0100 Subject: [PATCH 10/18] Add additional tests --- .../components/xiaomi_miio/config_flow.py | 4 +- .../xiaomi_miio/test_config_flow.py | 132 ++++++++++++++++++ 2 files changed, 133 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index d392d23c73b42a..7bf249fd989e8d 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -118,7 +118,6 @@ async def async_step_device(self, user_input=None): # Setup Gateways for gateway_model in MODELS_GATEWAY: if model.startswith(gateway_model): - mac = format_mac(device_info.mac_address) unique_id = mac await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() @@ -138,7 +137,6 @@ async def async_step_device(self, user_input=None): for device_model in MODELS_ALL_DEVICES: if model.startswith(device_model): - mac = format_mac(device_info.mac_address) unique_id = mac await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() @@ -162,7 +160,7 @@ async def async_step_device(self, user_input=None): schema = DEVICE_CONFIG if errors: - schema.extend(DEVICE_MODEL_CONFIG) + schema = schema.extend(DEVICE_MODEL_CONFIG) return self.async_show_form(step_id="device", data_schema=schema, errors=errors) diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 5e8ff8d293b5ab..63eafbc38f5a2a 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -257,6 +257,138 @@ 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.config_flow.get_mac_address", + return_value=TEST_MAC, + ): + 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: TEST_MAC, + } + + +async def test_config_flow_step_device_manual_model_fail(hass): + """Test config flow, device connection error, manual model fail.""" + 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.config_flow.get_mac_address", + return_value=None, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_TOKEN: TEST_TOKEN, const.CONF_MODEL: overwrite_model}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "device" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_config_flow_step_device_manual_model_err(hass): + """Test config flow, device connection error, manual model 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"} + + overwrite_model = const.MODELS_VACUUM[0] + + with patch( + "homeassistant.components.xiaomi_miio.device.Device.info", + side_effect=DeviceException({}), + ), patch( + "homeassistant.components.xiaomi_miio.config_flow.get_mac_address", + side_effect=OSError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_TOKEN: TEST_TOKEN, const.CONF_MODEL: overwrite_model}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "device" + assert result["errors"] == {"base": "cannot_connect"} + + 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( From 347425c2c24b86bd513bb7bb8b364e1422298b8e Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 20 Feb 2021 11:04:20 +0100 Subject: [PATCH 11/18] add missing async_setup_entry patch --- tests/components/xiaomi_miio/test_config_flow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 63eafbc38f5a2a..ce25a525b38c26 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -288,6 +288,8 @@ async def test_config_flow_step_device_manual_model_succes(hass): ), patch( "homeassistant.components.xiaomi_miio.config_flow.get_mac_address", return_value=TEST_MAC, + ), patch( + "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True ): result = await hass.config_entries.flow.async_configure( result["flow_id"], From 37cf186a2a3e8e87ce62e839c3d49cc9964dfca9 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 20 Feb 2021 11:20:48 +0100 Subject: [PATCH 12/18] Allow setting up without a mac --- .../components/xiaomi_miio/config_flow.py | 21 +++++++------- .../xiaomi_miio/test_config_flow.py | 28 +++++++++++++++---- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 7bf249fd989e8d..f6a17c6b916760 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -44,6 +44,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.""" @@ -57,15 +58,15 @@ async def async_step_zeroconf(self, discovery_info): """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}) @@ -76,7 +77,7 @@ async def async_step_zeroconf(self, discovery_info): return await self.async_step_device() for device_model in MODELS_ALL_DEVICES: 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) self._abort_if_unique_id_configured({CONF_HOST: self.host}) @@ -112,13 +113,13 @@ async def async_step_device(self, user_input=None): model = device_info.model if model is not None: - mac = await self.async_get_mac(device_info, self.host) + if self.mac is None: + self.mac = await self.async_get_mac(device_info, self.host) - if model is not None and mac is not None: # Setup Gateways for gateway_model in MODELS_GATEWAY: if model.startswith(gateway_model): - unique_id = mac + unique_id = self.mac await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() return self.async_create_entry( @@ -128,7 +129,7 @@ async def async_step_device(self, user_input=None): CONF_HOST: self.host, CONF_TOKEN: token, CONF_MODEL: model, - CONF_MAC: mac, + CONF_MAC: self.mac, }, ) @@ -137,7 +138,7 @@ async def async_step_device(self, user_input=None): for device_model in MODELS_ALL_DEVICES: if model.startswith(device_model): - unique_id = mac + unique_id = self.mac await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() return self.async_create_entry( @@ -147,7 +148,7 @@ async def async_step_device(self, user_input=None): CONF_HOST: self.host, CONF_TOKEN: token, CONF_MODEL: model, - CONF_MAC: mac, + CONF_MAC: self.mac, }, ) errors["base"] = "unknown_device" diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index ce25a525b38c26..a31411c33a61c6 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -338,15 +338,23 @@ async def test_config_flow_step_device_manual_model_fail(hass): ), patch( "homeassistant.components.xiaomi_miio.config_flow.get_mac_address", return_value=None, + ), 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"] == "form" - assert result["step_id"] == "device" - assert result["errors"] == {"base": "cannot_connect"} + 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 test_config_flow_step_device_manual_model_err(hass): @@ -380,15 +388,23 @@ async def test_config_flow_step_device_manual_model_err(hass): ), patch( "homeassistant.components.xiaomi_miio.config_flow.get_mac_address", side_effect=OSError, + ), 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"] == "form" - assert result["step_id"] == "device" - assert result["errors"] == {"base": "cannot_connect"} + 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): From 3eb5f2ed7b5e8d903790817973cbaba8ac0359d4 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 20 Feb 2021 11:32:18 +0100 Subject: [PATCH 13/18] Add getmac to requirements --- homeassistant/components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 1 + requirements_test_all.txt | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 2536b0e0aa7ab5..693c0aaea44444 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.10.56", "python-miio==0.5.4"], + "requirements": ["construct==2.10.56", "getmac==0.8.2", "python-miio==0.5.4"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG"], "zeroconf": ["_miio._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 24bb508c713de6..80827ce202961d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -651,6 +651,7 @@ georss_qld_bushfire_alert_client==0.3 # homeassistant.components.kef # homeassistant.components.minecraft_server # homeassistant.components.nmap_tracker +# homeassistant.components.xiaomi_miio getmac==0.8.2 # homeassistant.components.gios diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e22ab3fa0e2dfd..bd3f94b4f5033f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -345,6 +345,7 @@ georss_qld_bushfire_alert_client==0.3 # homeassistant.components.kef # homeassistant.components.minecraft_server # homeassistant.components.nmap_tracker +# homeassistant.components.xiaomi_miio getmac==0.8.2 # homeassistant.components.gios From d215fcf2026a3c8cf7415ecc817db37654434cb5 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 21 Feb 2021 19:38:28 +0100 Subject: [PATCH 14/18] remove getmac --- .../components/xiaomi_miio/config_flow.py | 27 +---- .../components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 1 - requirements_test_all.txt | 1 - .../xiaomi_miio/test_config_flow.py | 103 ------------------ 5 files changed, 3 insertions(+), 131 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index f6a17c6b916760..cfd59eb4b29f4e 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -2,7 +2,6 @@ from functools import partial import logging -from getmac import get_mac_address import voluptuous as vol from homeassistant import config_entries @@ -113,8 +112,8 @@ async def async_step_device(self, user_input=None): model = device_info.model if model is not None: - if self.mac is None: - self.mac = await self.async_get_mac(device_info, self.host) + 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: @@ -164,25 +163,3 @@ async def async_step_device(self, user_input=None): schema = schema.extend(DEVICE_MODEL_CONFIG) return self.async_show_form(step_id="device", data_schema=schema, errors=errors) - - async def async_get_mac(self, device_info, host): - """Get the mac address of a Miio Device when the info call fails.""" - if device_info is not None: - return format_mac(device_info.mac_address) - - # If the info call failed, use getmac as backup - try: - mac = await self.hass.async_add_executor_job( - partial(get_mac_address, **{"ip": host}) - ) - if not mac: - mac = await self.hass.async_add_executor_job( - partial(get_mac_address, **{"hostname": host}) - ) - except Exception as err: # pylint: disable=broad-except - _LOGGER.error("Unable to get mac address: %s", err) - mac = None - - if mac is not None: - mac = format_mac(mac) - return mac diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 693c0aaea44444..2536b0e0aa7ab5 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.10.56", "getmac==0.8.2", "python-miio==0.5.4"], + "requirements": ["construct==2.10.56", "python-miio==0.5.4"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG"], "zeroconf": ["_miio._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 80827ce202961d..24bb508c713de6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -651,7 +651,6 @@ georss_qld_bushfire_alert_client==0.3 # homeassistant.components.kef # homeassistant.components.minecraft_server # homeassistant.components.nmap_tracker -# homeassistant.components.xiaomi_miio getmac==0.8.2 # homeassistant.components.gios diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd3f94b4f5033f..e22ab3fa0e2dfd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -345,7 +345,6 @@ georss_qld_bushfire_alert_client==0.3 # homeassistant.components.kef # homeassistant.components.minecraft_server # homeassistant.components.nmap_tracker -# homeassistant.components.xiaomi_miio getmac==0.8.2 # homeassistant.components.gios diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index a31411c33a61c6..f4f7b5e2b46fe5 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -285,109 +285,6 @@ async def test_config_flow_step_device_manual_model_succes(hass): with patch( "homeassistant.components.xiaomi_miio.device.Device.info", side_effect=DeviceException({}), - ), patch( - "homeassistant.components.xiaomi_miio.config_flow.get_mac_address", - return_value=TEST_MAC, - ), 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: TEST_MAC, - } - - -async def test_config_flow_step_device_manual_model_fail(hass): - """Test config flow, device connection error, manual model fail.""" - 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.config_flow.get_mac_address", - return_value=None, - ), 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 test_config_flow_step_device_manual_model_err(hass): - """Test config flow, device connection error, manual model 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"} - - overwrite_model = const.MODELS_VACUUM[0] - - with patch( - "homeassistant.components.xiaomi_miio.device.Device.info", - side_effect=DeviceException({}), - ), patch( - "homeassistant.components.xiaomi_miio.config_flow.get_mac_address", - side_effect=OSError, ), patch( "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True ): From 3e6ddd435e251dcbbd7f7c5496b2c7964cdde198 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 21 Feb 2021 19:43:51 +0100 Subject: [PATCH 15/18] fix unused import --- homeassistant/components/xiaomi_miio/config_flow.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index cfd59eb4b29f4e..d7e2198f72f024 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -1,5 +1,4 @@ """Config flow to configure Xiaomi Miio.""" -from functools import partial import logging import voluptuous as vol From 627b80bf187ea4672e19601c607238156a22b6c2 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 21 Feb 2021 20:10:37 +0100 Subject: [PATCH 16/18] debug level --- homeassistant/components/xiaomi_miio/vacuum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 6d0651defe4b20..b34a64e9c7c2ef 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -148,7 +148,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): unique_id = config_entry.unique_id # Create handler - _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) + _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) vacuum = Vacuum(host, token) mirobo = MiroboVacuum(name, vacuum, config_entry, unique_id) From 6fceb702f4f0e195940a1cdb80f80574a131fbc8 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 21 Feb 2021 20:14:25 +0100 Subject: [PATCH 17/18] remove unused DATA_KEY --- homeassistant/components/xiaomi_miio/vacuum.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index b34a64e9c7c2ef..7bdbfca7bc9190 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -48,7 +48,6 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Vacuum cleaner" -DATA_KEY = "vacuum.xiaomi_miio" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -139,9 +138,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} - host = config_entry.data[CONF_HOST] token = config_entry.data[CONF_TOKEN] name = config_entry.title @@ -152,7 +148,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): vacuum = Vacuum(host, token) mirobo = MiroboVacuum(name, vacuum, config_entry, unique_id) - hass.data[DATA_KEY][host] = mirobo entities.append(mirobo) platform = entity_platform.current_platform.get() From d5c2a5215fd6146c68a3ef870dbdbedf9d212f11 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 21 Feb 2021 22:05:38 +0100 Subject: [PATCH 18/18] fix mac is None, device_info --- homeassistant/components/xiaomi_miio/device.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index 48bedbf0cc8ff3..cb91726ecadd3a 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -78,10 +78,14 @@ def name(self): @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