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 import async_timeout
from roombapy import Roomba, RoombaConnectionError 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.const import CONF_HOST, CONF_PASSWORD
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from .const import ( from .const import (
BLID, BLID,
COMPONENTS, COMPONENTS,
CONF_BLID, CONF_BLID,
CONF_CERT,
CONF_CONTINUOUS, CONF_CONTINUOUS,
CONF_DELAY, CONF_DELAY,
CONF_NAME, CONF_NAME,
DEFAULT_CERT,
DEFAULT_CONTINUOUS,
DEFAULT_DELAY,
DOMAIN, DOMAIN,
ROOMBA_SESSION, ROOMBA_SESSION,
) )
@ -29,54 +22,9 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _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): async def async_setup(hass, config):
"""Set up the roomba environment.""" """Set up the roomba environment."""
hass.data.setdefault(DOMAIN, {}) 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 return True
@ -88,8 +36,8 @@ async def async_setup_entry(hass, config_entry):
hass.config_entries.async_update_entry( hass.config_entries.async_update_entry(
config_entry, config_entry,
options={ options={
"continuous": config_entry.data[CONF_CONTINUOUS], CONF_CONTINUOUS: config_entry.data[CONF_CONTINUOUS],
"delay": config_entry.data[CONF_DELAY], CONF_DELAY: config_entry.data[CONF_DELAY],
}, },
) )
@ -184,12 +132,5 @@ def roomba_reported_state(roomba):
return roomba.master_state.get("state", {}).get("reported", {}) 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): class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect.""" """Error to indicate we cannot connect."""

View File

@ -1,5 +1,8 @@
"""Config flow to configure roomba component.""" """Config flow to configure roomba component."""
from roombapy import Roomba from roombapy import Roomba
from roombapy.discovery import RoombaDiscovery
from roombapy.getpassword import RoombaPassword
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, core from homeassistant import config_entries, core
@ -18,15 +21,9 @@ from .const import (
) )
from .const import DOMAIN # pylint:disable=unused-import from .const import DOMAIN # pylint:disable=unused-import
DATA_SCHEMA = vol.Schema( DEFAULT_OPTIONS = {CONF_CONTINUOUS: DEFAULT_CONTINUOUS, CONF_DELAY: DEFAULT_DELAY}
{
vol.Required(CONF_HOST): str, MAX_NUM_DEVICES_TO_DISCOVER = 25
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,
}
)
async def validate_input(hass: core.HomeAssistant, data): async def validate_input(hass: core.HomeAssistant, data):
@ -57,34 +54,156 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH 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 @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(config_entry):
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return OptionsFlowHandler(config_entry) 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): async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user.""" """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 = {} errors = {}
if user_input is not None: if user_input is not None:
await self.async_set_unique_id(user_input[CONF_BLID]) config = {
self._abort_if_unique_id_configured() CONF_HOST: self.host,
CONF_BLID: self.blid,
CONF_PASSWORD: user_input[CONF_PASSWORD],
**DEFAULT_OPTIONS,
}
try: try:
info = await validate_input(self.hass, user_input) info = await validate_input(self.hass, config)
except CannotConnect: except CannotConnect:
errors = {"base": "cannot_connect"} errors = {"base": "cannot_connect"}
if "base" not in errors: if not errors:
await async_disconnect_or_timeout(self.hass, info[ROOMBA_SESSION]) 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( 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", "domain": "roomba",
"name": "iRobot Roomba", "name": "iRobot Roomba and Braava",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/roomba", "documentation": "https://www.home-assistant.io/integrations/roomba",
"requirements": ["roombapy==1.6.2"], "requirements": ["roombapy==1.6.2"],

View File

@ -1,21 +1,39 @@
{ {
"config": { "config": {
"step": { "step": {
"user": { "init": {
"title": "Connect to the device", "title": "Automaticlly 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", "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": { "data": {
"host": "[%key:common::config_flow::data::host%]", "host": "[%key:common::config_flow::data::host%]",
"blid": "BLID", "blid": "BLID"
"password": "[%key:common::config_flow::data::password%]",
"continuous": "Continuous",
"delay": "Delay"
} }
} },
"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": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
} },
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
}, },
"options": { "options": {
"step": { "step": {

View File

@ -1,30 +1,48 @@
{ {
"config": { "config": {
"error": { "step": {
"cannot_connect": "Failed to connect" "init": {
}, "title": "Automaticlly connect to the device",
"step": { "description": "Select a Roomba or Braava.",
"user": { "data": {
"data": { "host": "[%key:common::config_flow::data::host%]"
"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"
}
} }
},
"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": { "error": {
"step": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
"init": { },
"data": { "abort": {
"continuous": "Continuous", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
"delay": "Delay" }
} },
} "options": {
"step": {
"init": {
"data": {
"continuous": "Continuous",
"delay": "Delay"
} }
}
} }
} }
}

View File

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