Add config_flow for roomba

This commit is contained in:
Cyr-ius
2020-03-25 13:51:29 +01:00
committed by cyr-ius
parent c8df5fb8ad
commit d539dfffe3
12 changed files with 488 additions and 63 deletions

View File

@@ -0,0 +1,45 @@
{
"config": {
"title": "iRobot Roomba",
"step": {
"user": {
"title": "Connect to the device",
"data": {
"host": "Hostname or IP Address",
"username": "Username",
"password": "Password",
"name": "Friendly Name",
"certificate": "Certificate",
"continuous": "Continuous",
"delay": "Delay"
}
}
},
"error": {
"cannot_connect": "Failed to connect, please try again",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"abort": {
"already_configured": "Device is already configured"
}
},
"options": {
"step": {
"init": {
"data": {
"certificate": "Certificate",
"continuous": "Continuous",
"delay": "Delay"
}
},
"options": {
"data": {
"certificate": "Certificate",
"continuous": "Continuous",
"delay": "Delay"
}
}
}
}
}

View File

@@ -0,0 +1,45 @@
{
"config": {
"title": "iRobot Roomba",
"step": {
"user": {
"title": "Connect to the device",
"data": {
"host": "Nom ou Addresse IP",
"username": "Utilisateur",
"password": "Mot de passe",
"name": "Nom",
"certificate": "Certificat",
"continuous": "Continue",
"delay": "Delais"
}
}
},
"error": {
"cannot_connect": "Failed to connect, please try again",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"abort": {
"already_configured": "Device is already configured"
}
},
"options": {
"step": {
"init": {
"data": {
"certificate": "Certificat",
"continuous": "Continue",
"delay": "Delais"
}
},
"options": {
"data": {
"certificate": "Certificate",
"continuous": "Continuous",
"delay": "Delay"
}
}
}
}
}

View File

@@ -1 +1,76 @@
"""The roomba component.""" """The roomba component."""
import logging
import async_timeout
from roomba import Roomba, RoombaConnectionError
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from .const import CONF_CERT, CONF_CONTINUOUS, CONF_DELAY, DOMAIN
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config):
"""Set up the roomba environment."""
if DOMAIN not in config:
return True
if not hass.config_entries.async_entries(DOMAIN):
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={}
)
)
async def async_setup_entry(hass, config_entry):
"""Set the config entry up."""
# Set up roomba platforms with config entry
if config_entry.data is None:
return False
if not config_entry.options:
hass.config_entries.async_update_entry(
config_entry,
options={
"certificate": config_entry.data[CONF_CERT],
"continuous": config_entry.data[CONF_CONTINUOUS],
"delay": config_entry.data[CONF_DELAY],
},
)
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
if "roomba" not in hass.data[DOMAIN]:
roomba = Roomba(
address=config_entry.data[CONF_HOST],
blid=config_entry.data[CONF_USERNAME],
password=config_entry.data[CONF_PASSWORD],
cert_name=config_entry.data[CONF_CERT],
continuous=config_entry.data[CONF_CONTINUOUS],
delay=config_entry.data[CONF_DELAY],
)
hass.data[DOMAIN]["roomba"] = roomba
try:
with async_timeout.timeout(9):
await hass.async_add_job(roomba.connect)
except RoombaConnectionError:
_LOGGER.error("Error to connect to {}".format(config_entry.data[CONF_HOST]))
return False
hass.data[DOMAIN]["name"] = config_entry.data[CONF_NAME]
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, "vacuum")
)
return True
async def async_unload_entry(hass, config_entry):
"""Unload a config entry."""
await hass.config_entries.async_forward_entry_unload(config_entry, "vacuum")
roomba = hass.data[DOMAIN]["roomba"]
await hass.async_add_job(roomba.disconnect)
return True

View File

@@ -0,0 +1,153 @@
"""Config flow to configure demo component."""
import logging
import time
from roomba import Roomba, RoombaConnectionError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from .const import (
CONF_CERT,
CONF_CONTINUOUS,
CONF_DELAY,
DEFAULT_CERT,
DEFAULT_CONTINUOUS,
DEFAULT_DELAY,
DEFAULT_NAME,
DOMAIN,
)
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): 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,
}
)
_LOGGER = logging.getLogger(__name__)
class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Demo configuration flow."""
VERSION = 1
@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 self.async_create_entry(title="Roomba", data={})
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
errors = {}
if DOMAIN not in self.hass.data:
self.hass.data[DOMAIN] = {}
if user_input is not None:
self.host = user_input[CONF_HOST]
self.username = user_input[CONF_USERNAME]
self.password = user_input[CONF_PASSWORD]
self.name = user_input[CONF_NAME]
self.certificate = user_input[CONF_CERT]
self.continuous = user_input[CONF_CONTINUOUS]
self.delay = user_input[CONF_DELAY]
roomba = Roomba(
address=self.host,
blid=self.username,
password=self.password,
cert_name=self.certificate,
continuous=self.continuous,
delay=self.delay,
)
_LOGGER.debug("Initializing communication with host %s", self.host)
try:
await self.hass.async_add_job(roomba.connect)
except RoombaConnectionError:
errors = {"base": "cannot_connect"}
timeout = time.time() + 1
while not roomba.roomba_connected and not errors:
if time.time() > timeout:
errors = {"base": "invalid_auth"}
await self.hass.async_add_job(roomba.disconnect)
time.sleep(0.2)
if roomba.roomba_connected:
self.hass.data[DOMAIN]["roomba"] = roomba
self.hass.data[DOMAIN]["name"] = self.name
return self.async_create_entry(
title=self.name,
data={
"host": self.host,
"username": self.username,
"password": self.password,
"name": self.name,
"certificate": self.certificate,
"continuous": self.continuous,
"delay": self.delay,
},
)
# If there was no user input, do not show the errors.
if user_input is None:
errors = {}
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options."""
def __init__(self, config_entry):
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init(self, user_input=None):
"""Manage the options."""
return await self.async_step_options()
async def async_step_options(self, user_input=None):
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="options",
data_schema=vol.Schema(
{
vol.Optional(
CONF_CERT,
default=self.config_entry.options.get(CONF_CERT, DEFAULT_CERT),
): str,
vol.Optional(
CONF_CONTINUOUS,
default=self.config_entry.options.get(
CONF_CONTINUOUS, DEFAULT_CONTINUOUS
),
): bool,
vol.Optional(
CONF_DELAY,
default=self.config_entry.options.get(
CONF_DELAY, DEFAULT_DELAY
),
): int,
}
),
)

View File

@@ -0,0 +1,9 @@
"""The roomba constants."""
DOMAIN = "roomba"
CONF_CERT = "certificate"
CONF_CONTINUOUS = "continuous"
CONF_DELAY = "delay"
DEFAULT_CERT = "/etc/ssl/certs/ca-certificates.crt"
DEFAULT_CONTINUOUS = True
DEFAULT_DELAY = 1
DEFAULT_NAME = "Roomba"

View File

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

View File

@@ -0,0 +1,45 @@
{
"config": {
"title": "iRobot Roomba",
"step": {
"user": {
"title": "Connect to the device",
"data": {
"host": "Hostname or IP Address",
"username": "Username",
"password": "Password",
"name": "Friendly Name",
"certificate": "Certificate",
"continuous": "Continuous",
"delay": "Delay"
}
}
},
"error": {
"cannot_connect": "Failed to connect, please try again",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"abort": {
"already_configured": "Device is already configured"
}
},
"options": {
"step": {
"init": {
"data": {
"certificate": "Certificate",
"continuous": "Continuous",
"delay": "Delay"
}
},
"options": {
"data": {
"certificate": "Certificate",
"continuous": "Continuous",
"delay": "Delay"
}
}
}
}
}

View File

@@ -1,13 +1,7 @@
"""Support for Wi-Fi enabled iRobot Roombas.""" """Support for Wi-Fi enabled iRobot Roombas."""
import asyncio
import logging import logging
import async_timeout
from roomba import Roomba
import voluptuous as vol
from homeassistant.components.vacuum import ( from homeassistant.components.vacuum import (
PLATFORM_SCHEMA,
SUPPORT_BATTERY, SUPPORT_BATTERY,
SUPPORT_FAN_SPEED, SUPPORT_FAN_SPEED,
SUPPORT_LOCATE, SUPPORT_LOCATE,
@@ -20,9 +14,8 @@ from homeassistant.components.vacuum import (
SUPPORT_TURN_ON, SUPPORT_TURN_ON,
VacuumDevice, VacuumDevice,
) )
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.exceptions import PlatformNotReady from .const import DOMAIN
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -37,34 +30,11 @@ ATTR_SOFTWARE_VERSION = "software_version"
CAP_POSITION = "position" CAP_POSITION = "position"
CAP_CARPET_BOOST = "carpet_boost" CAP_CARPET_BOOST = "carpet_boost"
CONF_CERT = "certificate"
CONF_CONTINUOUS = "continuous"
CONF_DELAY = "delay"
DEFAULT_CERT = "/etc/ssl/certs/ca-certificates.crt"
DEFAULT_CONTINUOUS = True
DEFAULT_DELAY = 1
DEFAULT_NAME = "Roomba"
PLATFORM = "roomba"
FAN_SPEED_AUTOMATIC = "Automatic" FAN_SPEED_AUTOMATIC = "Automatic"
FAN_SPEED_ECO = "Eco" FAN_SPEED_ECO = "Eco"
FAN_SPEED_PERFORMANCE = "Performance" FAN_SPEED_PERFORMANCE = "Performance"
FAN_SPEEDS = [FAN_SPEED_AUTOMATIC, FAN_SPEED_ECO, FAN_SPEED_PERFORMANCE] FAN_SPEEDS = [FAN_SPEED_AUTOMATIC, FAN_SPEED_ECO, FAN_SPEED_PERFORMANCE]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_CERT, default=DEFAULT_CERT): cv.string,
vol.Optional(CONF_CONTINUOUS, default=DEFAULT_CONTINUOUS): cv.boolean,
vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_int,
},
extra=vol.ALLOW_EXTRA,
)
# Commonly supported features # Commonly supported features
SUPPORT_ROOMBA = ( SUPPORT_ROOMBA = (
@@ -83,39 +53,12 @@ SUPPORT_ROOMBA = (
SUPPORT_ROOMBA_CARPET_BOOST = SUPPORT_ROOMBA | SUPPORT_FAN_SPEED SUPPORT_ROOMBA_CARPET_BOOST = SUPPORT_ROOMBA | SUPPORT_FAN_SPEED
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the iRobot Roomba vacuum cleaner platform.""" """Set up the iRobot Roomba vacuum cleaner."""
if PLATFORM not in hass.data:
hass.data[PLATFORM] = {}
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
certificate = config.get(CONF_CERT)
continuous = config.get(CONF_CONTINUOUS)
delay = config.get(CONF_DELAY)
roomba = Roomba(
address=host,
blid=username,
password=password,
cert_name=certificate,
continuous=continuous,
delay=delay,
)
_LOGGER.debug("Initializing communication with host %s", host)
try:
with async_timeout.timeout(9):
await hass.async_add_job(roomba.connect)
except asyncio.TimeoutError:
raise PlatformNotReady
name = hass.data[DOMAIN]["name"]
roomba = hass.data[DOMAIN]["roomba"]
roomba_vac = RoombaVacuum(name, roomba) roomba_vac = RoombaVacuum(name, roomba)
hass.data[PLATFORM][host] = roomba_vac
async_add_entities([roomba_vac], True) async_add_entities([roomba_vac], True)
@@ -135,6 +78,20 @@ class RoombaVacuum(VacuumDevice):
self.vacuum = roomba self.vacuum = roomba
self.vacuum_state = None self.vacuum_state = None
@property
def unique_id(self):
"""Return the uniqueid of the vacuum cleaner."""
return self._name
@property
def device_info(self):
"""Return the device info of the vacuum cleaner."""
return {
"identifiers": {(DOMAIN, self.unique_id)},
"manufacturer": "iRobots",
"name": str(self._name),
}
@property @property
def supported_features(self): def supported_features(self):
"""Flag vacuum cleaner robot features that are supported.""" """Flag vacuum cleaner robot features that are supported."""

View File

@@ -96,6 +96,7 @@ FLOWS = [
"rainmachine", "rainmachine",
"ring", "ring",
"roku", "roku",
"roomba",
"samsungtv", "samsungtv",
"sense", "sense",
"sentry", "sentry",

View File

@@ -678,6 +678,9 @@ ring_doorbell==0.6.0
# homeassistant.components.roku # homeassistant.components.roku
roku==4.1.0 roku==4.1.0
# homeassistant.components.roomba
roombapy==1.4.3
# homeassistant.components.yamaha # homeassistant.components.yamaha
rxv==0.6.0 rxv==0.6.0

View File

@@ -0,0 +1 @@
"""Tests for the iRobot Roomba integration."""

View File

@@ -0,0 +1,90 @@
"""Test the iRobot Roomba config flow."""
from asynctest import patch
from homeassistant import config_entries, setup
from homeassistant.components.roomba.config_flow import CannotConnect, InvalidAuth
from homeassistant.components.roomba.const import DOMAIN
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}
)
assert result["type"] == "form"
assert result["errors"] == {}
with patch(
"homeassistant.components.roomba.config_flow.PlaceholderHub.authenticate",
return_value=True,
), 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"],
{
"host": "1.1.1.1",
"username": "test-username",
"password": "test-password",
},
)
assert result2["type"] == "create_entry"
assert result2["title"] == "Name of the device"
assert result2["data"] == {
"host": "1.1.1.1",
"username": "test-username",
"password": "test-password",
}
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_invalid_auth(hass):
"""Test we handle invalid auth."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.roomba.config_flow.PlaceholderHub.authenticate",
side_effect=InvalidAuth,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"host": "1.1.1.1",
"username": "test-username",
"password": "test-password",
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "invalid_auth"}
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}
)
with patch(
"homeassistant.components.roomba.config_flow.PlaceholderHub.authenticate",
side_effect=CannotConnect,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"host": "1.1.1.1",
"username": "test-username",
"password": "test-password",
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "cannot_connect"}