Update roomba config flow to walk users through pairing (#45037)

* Update roomba config flow to walk users though pairing

* Remove YAML support

* adjust tests

* increase cover

* pylint

* pylint
This commit is contained in:
J. Nick Koston 2021-01-11 03:46:54 -10:00 committed by GitHub
parent eb5f3b282b
commit 74e7f7c879
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 624 additions and 174 deletions

View File

@ -4,24 +4,17 @@ import logging
import async_timeout
from roombapy import Roomba, RoombaConnectionError
import voluptuous as vol
from homeassistant import config_entries, exceptions
from homeassistant import exceptions
from homeassistant.const import CONF_HOST, CONF_PASSWORD
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from .const import (
BLID,
COMPONENTS,
CONF_BLID,
CONF_CERT,
CONF_CONTINUOUS,
CONF_DELAY,
CONF_NAME,
DEFAULT_CERT,
DEFAULT_CONTINUOUS,
DEFAULT_DELAY,
DOMAIN,
ROOMBA_SESSION,
)
@ -29,54 +22,9 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
def _has_all_unique_bilds(value):
"""Validate that each vacuum configured has a unique bild.
Uniqueness is determined case-independently.
"""
bilds = [device[CONF_BLID] for device in value]
schema = vol.Schema(vol.Unique())
schema(bilds)
return value
DEVICE_SCHEMA = vol.All(
cv.deprecated(CONF_CERT),
vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_BLID): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_CERT, default=DEFAULT_CERT): str,
vol.Optional(CONF_CONTINUOUS, default=DEFAULT_CONTINUOUS): bool,
vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): int,
},
),
)
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.All(cv.ensure_list, [DEVICE_SCHEMA], _has_all_unique_bilds)},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass, config):
"""Set up the roomba environment."""
hass.data.setdefault(DOMAIN, {})
if DOMAIN not in config:
return True
for index, conf in enumerate(config[DOMAIN]):
_LOGGER.debug("Importing Roomba #%d - %s", index, conf[CONF_HOST])
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=conf,
)
)
return True
@ -88,8 +36,8 @@ async def async_setup_entry(hass, config_entry):
hass.config_entries.async_update_entry(
config_entry,
options={
"continuous": config_entry.data[CONF_CONTINUOUS],
"delay": config_entry.data[CONF_DELAY],
CONF_CONTINUOUS: config_entry.data[CONF_CONTINUOUS],
CONF_DELAY: config_entry.data[CONF_DELAY],
},
)
@ -184,12 +132,5 @@ def roomba_reported_state(roomba):
return roomba.master_state.get("state", {}).get("reported", {})
@callback
def _async_find_matching_config_entry(hass, prefix):
for entry in hass.config_entries.async_entries(DOMAIN):
if entry.unique_id == prefix:
return entry
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""

View File

@ -1,5 +1,8 @@
"""Config flow to configure roomba component."""
from roombapy import Roomba
from roombapy.discovery import RoombaDiscovery
from roombapy.getpassword import RoombaPassword
import voluptuous as vol
from homeassistant import config_entries, core
@ -18,15 +21,9 @@ from .const import (
)
from .const import DOMAIN # pylint:disable=unused-import
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_BLID): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_CONTINUOUS, default=DEFAULT_CONTINUOUS): bool,
vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): int,
}
)
DEFAULT_OPTIONS = {CONF_CONTINUOUS: DEFAULT_CONTINUOUS, CONF_DELAY: DEFAULT_DELAY}
MAX_NUM_DEVICES_TO_DISCOVER = 25
async def validate_input(hass: core.HomeAssistant, data):
@ -57,34 +54,156 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
def __init__(self):
"""Initialize the roomba flow."""
self.discovered_robots = {}
self.name = None
self.blid = None
self.host = None
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)
async def async_step_import(self, import_info):
"""Set the config entry up from yaml."""
return await self.async_step_user(import_info)
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
# This is for backwards compatibility.
return await self.async_step_init(user_input)
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
# Check if user chooses manual entry
if user_input is not None and not user_input.get(CONF_HOST):
return await self.async_step_manual()
if (
user_input is not None
and self.discovered_robots is not None
and user_input[CONF_HOST] in self.discovered_robots
):
self.host = user_input[CONF_HOST]
device = self.discovered_robots[self.host]
self.blid = device.blid
self.name = device.robot_name
await self.async_set_unique_id(self.blid, raise_on_progress=False)
self._abort_if_unique_id_configured()
return await self.async_step_link()
already_configured = self._async_current_ids(False)
discovery = _async_get_roomba_discovery()
devices = await self.hass.async_add_executor_job(discovery.get_all)
if devices:
# Find already configured hosts
self.discovered_robots = {
device.ip: device
for device in devices
if device.blid not in already_configured
}
if not self.discovered_robots:
return await self.async_step_manual()
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional("host"): vol.In(
{
**{
device.ip: f"{device.robot_name} ({device.ip})"
for device in devices
if device.blid not in already_configured
},
None: "Manually add a Roomba or Braava",
}
)
}
),
)
async def async_step_manual(self, user_input=None):
"""Handle manual device setup."""
if user_input is None:
return self.async_show_form(
step_id="manual",
data_schema=vol.Schema(
{vol.Required(CONF_HOST): str, vol.Required(CONF_BLID): str}
),
)
if any(
user_input["host"] == entry.data.get("host")
for entry in self._async_current_entries()
):
return self.async_abort(reason="already_configured")
self.host = user_input[CONF_HOST]
self.blid = user_input[CONF_BLID]
await self.async_set_unique_id(self.blid, raise_on_progress=False)
self._abort_if_unique_id_configured()
return await self.async_step_link()
async def async_step_link(self, user_input=None):
"""Attempt to link with the Roomba.
Given a configured host, will ask the user to press the home and target buttons
to connect to the device.
"""
if user_input is None:
return self.async_show_form(step_id="link")
password = await self.hass.async_add_executor_job(
RoombaPassword(self.host).get_password
)
if not password:
return await self.async_step_link_manual()
config = {
CONF_HOST: self.host,
CONF_BLID: self.blid,
CONF_PASSWORD: password,
**DEFAULT_OPTIONS,
}
if not self.name:
try:
info = await validate_input(self.hass, config)
except CannotConnect:
return self.async_abort(reason="cannot_connect")
await async_disconnect_or_timeout(self.hass, info[ROOMBA_SESSION])
self.name = info[CONF_NAME]
return self.async_create_entry(title=self.name, data=config)
async def async_step_link_manual(self, user_input=None):
"""Handle manual linking."""
errors = {}
if user_input is not None:
await self.async_set_unique_id(user_input[CONF_BLID])
self._abort_if_unique_id_configured()
config = {
CONF_HOST: self.host,
CONF_BLID: self.blid,
CONF_PASSWORD: user_input[CONF_PASSWORD],
**DEFAULT_OPTIONS,
}
try:
info = await validate_input(self.hass, user_input)
info = await validate_input(self.hass, config)
except CannotConnect:
errors = {"base": "cannot_connect"}
if "base" not in errors:
if not errors:
await async_disconnect_or_timeout(self.hass, info[ROOMBA_SESSION])
return self.async_create_entry(title=info[CONF_NAME], data=user_input)
return self.async_create_entry(title=info[CONF_NAME], data=config)
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
step_id="link_manual",
data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}),
errors=errors,
)
@ -119,3 +238,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
}
),
)
@callback
def _async_get_roomba_discovery():
"""Create a discovery object."""
discovery = RoombaDiscovery()
discovery.amount_of_broadcasted_messages = MAX_NUM_DEVICES_TO_DISCOVER
return discovery

View File

@ -1,6 +1,6 @@
{
"domain": "roomba",
"name": "iRobot Roomba",
"name": "iRobot Roomba and Braava",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/roomba",
"requirements": ["roombapy==1.6.2"],

View File

@ -1,21 +1,39 @@
{
"config": {
"step": {
"user": {
"title": "Connect to the device",
"description": "Currently retrieving the BLID and password is a manual process. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
"init": {
"title": "Automaticlly connect to the device",
"description": "Select a Roomba or Braava.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
},
"manual": {
"title": "Manually connect to the device",
"description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-`. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"blid": "BLID",
"password": "[%key:common::config_flow::data::password%]",
"continuous": "Continuous",
"delay": "Delay"
"blid": "BLID"
}
}
},
"link": {
"title": "Retrieve Password",
"description": "Press and hold the Home button until the device generates a sound (about two seconds)."
},
"link_manual": {
"title": "Enter Password",
"description": "The password could not be retrivied from the device automaticlly. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
"data": {
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"options": {
"step": {

View File

@ -1,30 +1,48 @@
{
"config": {
"error": {
"cannot_connect": "Failed to connect"
},
"step": {
"user": {
"data": {
"blid": "BLID",
"continuous": "Continuous",
"delay": "Delay",
"host": "Host",
"password": "Password"
},
"description": "Currently retrieving the BLID and password is a manual process. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
"title": "Connect to the device"
}
"config": {
"step": {
"init": {
"title": "Automaticlly connect to the device",
"description": "Select a Roomba or Braava.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
},
"manual": {
"title": "Manually connect to the device",
"description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-`. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"blid": "BLID"
}
},
"link": {
"title": "Retrieve Password",
"description": "Press and hold the Home button until the device generates a sound (about two seconds)."
},
"link_manual": {
"title": "Enter Password",
"description": "The password could not be retrivied from the device automaticlly. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
"data": {
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"options": {
"step": {
"init": {
"data": {
"continuous": "Continuous",
"delay": "Delay"
}
}
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"options": {
"step": {
"init": {
"data": {
"continuous": "Continuous",
"delay": "Delay"
}
}
}
}
}
}

View File

@ -2,6 +2,7 @@
from unittest.mock import MagicMock, PropertyMock, patch
from roombapy import RoombaConnectionError
from roombapy.roomba import RoombaInfo
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.roomba.const import (
@ -14,16 +15,9 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD
from tests.common import MockConfigEntry
MOCK_IP = "1.2.3.4"
VALID_CONFIG = {CONF_HOST: "1.2.3.4", CONF_BLID: "blid", CONF_PASSWORD: "password"}
VALID_YAML_CONFIG = {
CONF_HOST: "1.2.3.4",
CONF_BLID: "blid",
CONF_PASSWORD: "password",
CONF_CONTINUOUS: True,
CONF_DELAY: 1,
}
def _create_mocked_roomba(
roomba_connected=None, master_state=None, connect=None, disconnect=None
@ -36,55 +30,227 @@ def _create_mocked_roomba(
return mocked_roomba
async def test_form(hass):
"""Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
def _mocked_discovery(*_):
roomba_discovery = MagicMock()
roomba = RoombaInfo(
hostname="iRobot-blid",
robot_name="robot_name",
ip=MOCK_IP,
mac="mac",
firmware="firmware",
sku="sku",
capabilities="capabilities",
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
roomba_discovery.get_all = MagicMock(return_value=[roomba])
return roomba_discovery
def _mocked_failed_discovery(*_):
roomba_discovery = MagicMock()
roomba_discovery.get_all = MagicMock(return_value=[])
return roomba_discovery
def _mocked_getpassword(*_):
roomba_password = MagicMock()
roomba_password.get_password = MagicMock(return_value="password")
return roomba_password
def _mocked_failed_getpassword(*_):
roomba_password = MagicMock()
roomba_password.get_password = MagicMock(return_value=None)
return roomba_password
async def test_form_user_discovery_and_password_fetch(hass):
"""Test we can discovery and fetch the password."""
await setup.async_setup_component(hass, "persistent_notification", {})
mocked_roomba = _create_mocked_roomba(
roomba_connected=True,
master_state={"state": {"reported": {"name": "myroomba"}}},
)
with patch(
"homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] is None
assert result["step_id"] == "init"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: MOCK_IP},
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["errors"] is None
assert result2["step_id"] == "link"
with patch(
"homeassistant.components.roomba.config_flow.Roomba",
return_value=mocked_roomba,
), patch(
"homeassistant.components.roomba.config_flow.RoombaPassword",
_mocked_getpassword,
), patch(
"homeassistant.components.roomba.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.roomba.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
VALID_CONFIG,
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{},
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "myroomba"
assert result2["result"].unique_id == "blid"
assert result2["data"] == {
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result3["title"] == "robot_name"
assert result3["result"].unique_id == "blid"
assert result3["data"] == {
CONF_BLID: "blid",
CONF_CONTINUOUS: True,
CONF_DELAY: 1,
CONF_HOST: "1.2.3.4",
CONF_HOST: MOCK_IP,
CONF_PASSWORD: "password",
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_cannot_connect(hass):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
async def test_form_user_discovery_skips_known(hass):
"""Test discovery proceeds to manual if all discovered are already known."""
await setup.async_setup_component(hass, "persistent_notification", {})
entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, unique_id="blid")
entry.add_to_hass(hass)
with patch(
"homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] is None
assert result["step_id"] == "manual"
async def test_form_user_failed_discovery_aborts_already_configured(hass):
"""Test if we manually configure an existing host we abort."""
await setup.async_setup_component(hass, "persistent_notification", {})
entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, unique_id="blid")
entry.add_to_hass(hass)
with patch(
"homeassistant.components.roomba.config_flow.RoombaDiscovery",
_mocked_failed_discovery,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] is None
assert result["step_id"] == "manual"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: MOCK_IP, CONF_BLID: "blid"},
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result2["reason"] == "already_configured"
async def test_form_user_discovery_manual_and_auto_password_fetch(hass):
"""Test discovery skipped and we can auto fetch the password."""
await setup.async_setup_component(hass, "persistent_notification", {})
mocked_roomba = _create_mocked_roomba(
roomba_connected=True,
master_state={"state": {"reported": {"name": "myroomba"}}},
)
with patch(
"homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] is None
assert result["step_id"] == "init"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: None},
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["errors"] is None
assert result2["step_id"] == "manual"
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{CONF_HOST: MOCK_IP, CONF_BLID: "blid"},
)
await hass.async_block_till_done()
assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result3["errors"] is None
with patch(
"homeassistant.components.roomba.config_flow.Roomba",
return_value=mocked_roomba,
), patch(
"homeassistant.components.roomba.config_flow.RoombaPassword",
_mocked_getpassword,
), patch(
"homeassistant.components.roomba.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.roomba.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result4 = await hass.config_entries.flow.async_configure(
result3["flow_id"],
{},
)
await hass.async_block_till_done()
assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result4["title"] == "myroomba"
assert result4["result"].unique_id == "blid"
assert result4["data"] == {
CONF_BLID: "blid",
CONF_CONTINUOUS: True,
CONF_DELAY: 1,
CONF_HOST: MOCK_IP,
CONF_PASSWORD: "password",
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_connect(
hass,
):
"""Test discovery skipped and we can auto fetch the password then we fail to connect."""
await setup.async_setup_component(hass, "persistent_notification", {})
mocked_roomba = _create_mocked_roomba(
connect=RoombaConnectionError,
@ -92,27 +258,161 @@ async def test_form_cannot_connect(hass):
master_state={"state": {"reported": {"name": "myroomba"}}},
)
with patch(
"homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] is None
assert result["step_id"] == "init"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: None},
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["errors"] is None
assert result2["step_id"] == "manual"
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{CONF_HOST: MOCK_IP, CONF_BLID: "blid"},
)
await hass.async_block_till_done()
assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result3["errors"] is None
with patch(
"homeassistant.components.roomba.config_flow.Roomba",
return_value=mocked_roomba,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
VALID_CONFIG,
), patch(
"homeassistant.components.roomba.config_flow.RoombaPassword",
_mocked_getpassword,
), patch(
"homeassistant.components.roomba.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.roomba.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result4 = await hass.config_entries.flow.async_configure(
result3["flow_id"],
{},
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["errors"] == {"base": "cannot_connect"}
assert result4["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result4["reason"] == "cannot_connect"
assert len(mock_setup.mock_calls) == 0
assert len(mock_setup_entry.mock_calls) == 0
async def test_form_import(hass):
"""Test we can import yaml config."""
async def test_form_user_discovery_fails_and_auto_password_fetch(hass):
"""Test discovery fails and we can auto fetch the password."""
await setup.async_setup_component(hass, "persistent_notification", {})
mocked_roomba = _create_mocked_roomba(
roomba_connected=True,
master_state={"state": {"reported": {"name": "imported_roomba"}}},
master_state={"state": {"reported": {"name": "myroomba"}}},
)
with patch(
"homeassistant.components.roomba.config_flow.RoombaDiscovery",
_mocked_failed_discovery,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] is None
assert result["step_id"] == "manual"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: MOCK_IP, CONF_BLID: "blid"},
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["errors"] is None
with patch(
"homeassistant.components.roomba.config_flow.Roomba",
return_value=mocked_roomba,
), patch(
"homeassistant.components.roomba.config_flow.RoombaPassword",
_mocked_getpassword,
), patch(
"homeassistant.components.roomba.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.roomba.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{},
)
await hass.async_block_till_done()
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result3["title"] == "myroomba"
assert result3["result"].unique_id == "blid"
assert result3["data"] == {
CONF_BLID: "blid",
CONF_CONTINUOUS: True,
CONF_DELAY: 1,
CONF_HOST: MOCK_IP,
CONF_PASSWORD: "password",
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_user_discovery_fails_and_password_fetch_fails(hass):
"""Test discovery fails and password fetch fails."""
await setup.async_setup_component(hass, "persistent_notification", {})
mocked_roomba = _create_mocked_roomba(
roomba_connected=True,
master_state={"state": {"reported": {"name": "myroomba"}}},
)
with patch(
"homeassistant.components.roomba.config_flow.RoombaDiscovery",
_mocked_failed_discovery,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] is None
assert result["step_id"] == "manual"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: MOCK_IP, CONF_BLID: "blid"},
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["errors"] is None
with patch(
"homeassistant.components.roomba.config_flow.RoombaPassword",
_mocked_failed_getpassword,
):
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{},
)
await hass.async_block_till_done()
with patch(
"homeassistant.components.roomba.config_flow.Roomba",
return_value=mocked_roomba,
@ -122,39 +422,85 @@ async def test_form_import(hass):
"homeassistant.components.roomba.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=VALID_YAML_CONFIG.copy(),
result4 = await hass.config_entries.flow.async_configure(
result3["flow_id"],
{CONF_PASSWORD: "password"},
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["result"].unique_id == "blid"
assert result["title"] == "imported_roomba"
assert result["data"] == {
assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result4["title"] == "myroomba"
assert result4["result"].unique_id == "blid"
assert result4["data"] == {
CONF_BLID: "blid",
CONF_CONTINUOUS: True,
CONF_DELAY: 1,
CONF_HOST: "1.2.3.4",
CONF_HOST: MOCK_IP,
CONF_PASSWORD: "password",
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_import_dupe(hass):
"""Test we get abort on duplicate import."""
async def test_form_user_discovery_fails_and_password_fetch_fails_and_cannot_connect(
hass,
):
"""Test discovery fails and password fetch fails then we cannot connect."""
await setup.async_setup_component(hass, "persistent_notification", {})
entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, unique_id="blid")
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=VALID_YAML_CONFIG.copy(),
mocked_roomba = _create_mocked_roomba(
connect=RoombaConnectionError,
roomba_connected=True,
master_state={"state": {"reported": {"name": "myroomba"}}},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
with patch(
"homeassistant.components.roomba.config_flow.RoombaDiscovery",
_mocked_failed_discovery,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] is None
assert result["step_id"] == "manual"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: MOCK_IP, CONF_BLID: "blid"},
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["errors"] is None
with patch(
"homeassistant.components.roomba.config_flow.RoombaPassword",
_mocked_failed_getpassword,
):
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{},
)
await hass.async_block_till_done()
with patch(
"homeassistant.components.roomba.config_flow.Roomba",
return_value=mocked_roomba,
), patch(
"homeassistant.components.roomba.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.roomba.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result4 = await hass.config_entries.flow.async_configure(
result3["flow_id"],
{CONF_PASSWORD: "password"},
)
await hass.async_block_till_done()
assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result4["errors"] == {"base": "cannot_connect"}
assert len(mock_setup.mock_calls) == 0
assert len(mock_setup_entry.mock_calls) == 0