Add config_flow for Roomba (#33302)

* Add config_flow for roomba

* Get options to connect

* Fix options in config flow

* Fix syntax in config_flow

* Remove name (not necessary)

* Add bin sensor

* Add Battery sensor

* Add async_connect

* Fix typo

* Add Model and Software version

* Update Roombapy 1.5.0

* Add validate_input

* Add connect and disconnect functions

* Remove test config flow

* Add variables after loop

* Fix translate

* Fix typo

* Fix state of bin

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/config_flow.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/config_flow.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/config_flow.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/config_flow.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/config_flow.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/config_flow.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Remove invalid auth

* Add call function reported_state

* Add options reload

* Fix tracelog

* Set entry_id for config_entry

* Fix DOMAIN unsed-import

* Update homeassistant/components/roomba/config_flow.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/config_flow.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Add unique_id for entry

* Fix device info

* Add config_flow for roomba

* Get options to connect

* Fix options in config flow

* Fix syntax in config_flow

* Remove name (not necessary)

* Add bin sensor

* Add Battery sensor

* Add async_connect

* Fix typo

* Add Model and Software version

* Update Roombapy 1.5.0

* Add validate_input

* Add connect and disconnect functions

* Remove test config flow

* Add variables after loop

* Fix translate

* Fix typo

* Fix state of bin

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/config_flow.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/config_flow.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/config_flow.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/config_flow.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/config_flow.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/config_flow.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Remove invalid auth

* Add call function reported_state

* Add options reload

* Fix tracelog

* Set entry_id for config_entry

* Fix DOMAIN unsed-import

* Update homeassistant/components/roomba/config_flow.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/config_flow.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Add unique_id for entry

* Fix device info

* syntax for mac (pyupgrade)

* Change single key to BLID

* Resolve dict conflict

* Update homeassistant/components/roomba/binary_sensor.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/sensor.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/.translations/en.json

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/strings.json

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Add description

* Revert "Remove test config flow"

This reverts commit 26a89422e89d7c88dd3c0ec3066e607afdc99f09.

* Add tests

* Remove check if user none

* Replace CONF_USERNAME to CONF_BLID (breaking change)

* Update test_config_flow.py

* Add code owners

* Remove CONF_USERNAME (unused)

* Add multiple vacuum

* Add multiple vacuum

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Fix syntax name

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Remove CONF_PREFIX (unused)

* Update homeassistant/components/roomba/sensor.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Add import UNIT_PERCENTAGE

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/roomba/__init__.py

Co-Authored-By: J. Nick Koston <nick@koston.org>

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Save me 2020-04-11 17:55:00 +02:00 committed by GitHub
parent d5f73378f0
commit 8c4a139aeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 784 additions and 72 deletions

View File

@ -314,7 +314,7 @@ homeassistant/components/rfxtrx/* @danielhiversen
homeassistant/components/ring/* @balloob
homeassistant/components/rmvtransport/* @cgtobi
homeassistant/components/roku/* @ctalkington
homeassistant/components/roomba/* @pschmitt
homeassistant/components/roomba/* @pschmitt @cyr-ius
homeassistant/components/safe_mode/* @home-assistant/core
homeassistant/components/saj/* @fredericvl
homeassistant/components/salt/* @bjornorri

View File

@ -0,0 +1,33 @@
{
"config": {
"title": "iRobot Roomba",
"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",
"data": {
"host": "Hostname or IP Address",
"blid": "BLID",
"password": "Password",
"certificate": "Certificate",
"continuous": "Continuous",
"delay": "Delay"
}
}
},
"error": {
"unknown" : "Unexpected error",
"cannot_connect": "Failed to connect, please try again"
}
},
"options": {
"step": {
"init": {
"data": {
"continuous": "Continuous",
"delay": "Delay"
}
}
}
}
}

View File

@ -0,0 +1,33 @@
{
"config": {
"title": "iRobot Roomba",
"step": {
"user": {
"title": "Connexion au périphérique",
"description": "Actuellement la récupération du BLID et du mot de passe nécessite une procédure manuelle. Veuillez suivre les étapes décrites dans la documentation sur: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials",
"data": {
"host": "Nom ou Addresse IP",
"blid": "BLID",
"password": "Mot de passe",
"certificate": "Certificat",
"continuous": "Continue",
"delay": "Delais"
}
}
},
"error": {
"unknown" : "Erreur imprévu",
"cannot_connect": "Impossible de se connecter"
}
},
"options": {
"step": {
"init": {
"data": {
"continuous": "Continue",
"delay": "Delais"
}
}
}
}
}

View File

@ -1 +1,191 @@
"""The roomba component."""
import asyncio
import logging
import async_timeout
from roomba import Roomba, RoombaConnectionError
import voluptuous as vol
from homeassistant import config_entries, 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,
)
_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.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
async def async_setup_entry(hass, config_entry):
"""Set the config entry up."""
# Set up roomba platforms with config entry
if not config_entry.options:
hass.config_entries.async_update_entry(
config_entry,
options={
"continuous": config_entry.data[CONF_CONTINUOUS],
"delay": config_entry.data[CONF_DELAY],
},
)
roomba = Roomba(
address=config_entry.data[CONF_HOST],
blid=config_entry.data[CONF_BLID],
password=config_entry.data[CONF_PASSWORD],
cert_name=config_entry.data[CONF_CERT],
continuous=config_entry.options[CONF_CONTINUOUS],
delay=config_entry.options[CONF_DELAY],
)
try:
if not await async_connect_or_timeout(hass, roomba):
return False
except CannotConnect:
raise exceptions.ConfigEntryNotReady
hass.data[DOMAIN][config_entry.entry_id] = {
ROOMBA_SESSION: roomba,
BLID: config_entry.data[CONF_BLID],
}
for component in COMPONENTS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, component)
)
if not config_entry.update_listeners:
config_entry.add_update_listener(async_update_options)
return True
async def async_connect_or_timeout(hass, roomba):
"""Connect to vacuum."""
try:
name = None
with async_timeout.timeout(10):
_LOGGER.debug("Initialize connection to vacuum")
await hass.async_add_job(roomba.connect)
while not roomba.roomba_connected or name is None:
# Waiting for connection and check datas ready
name = roomba_reported_state(roomba).get("name", None)
if name:
break
await asyncio.sleep(1)
except RoombaConnectionError:
_LOGGER.error("Error to connect to vacuum")
raise CannotConnect
except asyncio.TimeoutError:
# api looping if user or password incorrect and roomba exist
await async_disconnect_or_timeout(hass, roomba)
_LOGGER.error("Timeout expired")
raise CannotConnect
return {ROOMBA_SESSION: roomba, CONF_NAME: name}
async def async_disconnect_or_timeout(hass, roomba):
"""Disconnect to vacuum."""
_LOGGER.debug("Disconnect vacuum")
with async_timeout.timeout(3):
await hass.async_add_job(roomba.disconnect)
return True
async def async_update_options(hass, config_entry):
"""Update options."""
await hass.config_entries.async_reload(config_entry.entry_id)
async def async_unload_entry(hass, config_entry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(config_entry, component)
for component in COMPONENTS
]
)
)
if unload_ok:
domain_data = hass.data[DOMAIN][config_entry.entry_id]
await async_disconnect_or_timeout(hass, roomba=domain_data[ROOMBA_SESSION])
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok
def roomba_reported_state(roomba):
"""Roomba report."""
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

@ -0,0 +1,74 @@
"""Roomba binary sensor entities."""
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from . import roomba_reported_state
from .const import BLID, DOMAIN, ROOMBA_SESSION
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the iRobot Roomba vacuum cleaner."""
domain_data = hass.data[DOMAIN][config_entry.entry_id]
roomba = domain_data[ROOMBA_SESSION]
blid = domain_data[BLID]
status = roomba_reported_state(roomba).get("bin", {})
if "full" in status:
roomba_vac = RoombaBinStatus(roomba, blid)
async_add_entities([roomba_vac], True)
class RoombaBinStatus(BinarySensorDevice):
"""Class to hold Roomba Sensor basic info."""
ICON = "mdi:delete-variant"
def __init__(self, roomba, blid):
"""Initialize the sensor object."""
self.vacuum = roomba
self.vacuum_state = roomba_reported_state(roomba)
self._blid = blid
self._name = self.vacuum_state.get("name")
self._identifier = f"roomba_{self._blid}"
self._bin_status = None
@property
def name(self):
"""Return the name of the sensor."""
return f"{self._name} Bin Full"
@property
def unique_id(self):
"""Return the ID of this sensor."""
return f"bin_{self._blid}"
@property
def icon(self):
"""Return the icon of this sensor."""
return self.ICON
@property
def state(self):
"""Return the state of the sensor."""
return self._bin_status
@property
def device_info(self):
"""Return the device info of the vacuum cleaner."""
return {
"identifiers": {(DOMAIN, self._identifier)},
"name": str(self._name),
}
async def async_update(self):
"""Return the update info of the vacuum cleaner."""
# No data, no update
if not self.vacuum.master_state:
_LOGGER.debug("Roomba %s has no data yet. Skip update", self.name)
return
self._bin_status = (
roomba_reported_state(self.vacuum).get("bin", {}).get("full", False)
)
_LOGGER.debug("Update Full Bin status from the vacuum: %s", self._bin_status)

View File

@ -0,0 +1,131 @@
"""Config flow to configure roomba component."""
import logging
from roomba import Roomba
import voluptuous as vol
from homeassistant import config_entries, core
from homeassistant.const import CONF_HOST, CONF_PASSWORD
from homeassistant.core import callback
from . import CannotConnect, async_connect_or_timeout, async_disconnect_or_timeout
from .const import (
CONF_BLID,
CONF_CERT,
CONF_CONTINUOUS,
CONF_DELAY,
CONF_NAME,
DEFAULT_CERT,
DEFAULT_CONTINUOUS,
DEFAULT_DELAY,
ROOMBA_SESSION,
)
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_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__)
async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
roomba = Roomba(
address=data[CONF_HOST],
blid=data[CONF_BLID],
password=data[CONF_PASSWORD],
cert_name=data[CONF_CERT],
continuous=data[CONF_CONTINUOUS],
delay=data[CONF_DELAY],
)
info = await async_connect_or_timeout(hass, roomba)
return {
ROOMBA_SESSION: info[ROOMBA_SESSION],
CONF_NAME: info[CONF_NAME],
CONF_HOST: data[CONF_HOST],
}
class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Roomba configuration flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
@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."""
errors = {}
if user_input is not None:
await self.async_set_unique_id(user_input[CONF_BLID])
self._abort_if_unique_id_configured()
try:
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors = {"base": "cannot_connect"}
except Exception: # pylint: disable=broad-except
errors = {"base": "unknown"}
if "base" not in 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_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."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
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,13 @@
"""The roomba constants."""
DOMAIN = "roomba"
COMPONENTS = ["sensor", "binary_sensor", "vacuum"]
CONF_CERT = "certificate"
CONF_CONTINUOUS = "continuous"
CONF_DELAY = "delay"
CONF_NAME = "name"
CONF_BLID = "blid"
DEFAULT_CERT = "/etc/ssl/certs/ca-certificates.crt"
DEFAULT_CONTINUOUS = True
DEFAULT_DELAY = 1
ROOMBA_SESSION = "roomba_session"
BLID = "blid_key"

View File

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

View File

@ -0,0 +1,76 @@
"""Sensor for checking the battery level of Roomba."""
import logging
from homeassistant.const import DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE
from homeassistant.helpers.entity import Entity
from . import roomba_reported_state
from .const import BLID, DOMAIN, ROOMBA_SESSION
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the iRobot Roomba vacuum cleaner."""
domain_data = hass.data[DOMAIN][config_entry.entry_id]
roomba = domain_data[ROOMBA_SESSION]
blid = domain_data[BLID]
roomba_vac = RoombaBattery(roomba, blid)
async_add_entities([roomba_vac], True)
class RoombaBattery(Entity):
"""Class to hold Roomba Sensor basic info."""
def __init__(self, roomba, blid):
"""Initialize the sensor object."""
self.vacuum = roomba
self.vacuum_state = roomba_reported_state(roomba)
self._blid = blid
self._name = self.vacuum_state.get("name")
self._identifier = f"roomba_{self._blid}"
self._battery_level = None
@property
def name(self):
"""Return the name of the sensor."""
return f"{self._name} Battery Level"
@property
def unique_id(self):
"""Return the ID of this sensor."""
return f"battery_{self._blid}"
@property
def device_class(self):
"""Return the device class of the sensor."""
return DEVICE_CLASS_BATTERY
@property
def unit_of_measurement(self):
"""Return the unit_of_measurement of the device."""
return UNIT_PERCENTAGE
@property
def state(self):
"""Return the state of the sensor."""
return self._battery_level
@property
def device_info(self):
"""Return the device info of the vacuum cleaner."""
return {
"identifiers": {(DOMAIN, self._identifier)},
"name": str(self._name),
}
async def async_update(self):
"""Return the update info of the vacuum cleaner."""
# No data, no update
if not self.vacuum.master_state:
_LOGGER.debug("Roomba %s has no data yet. Skip update", self.name)
return
self._battery_level = roomba_reported_state(self.vacuum).get("batPct")
_LOGGER.debug(
"Update battery level status from the vacuum: %s", self._battery_level
)

View File

@ -0,0 +1,33 @@
{
"config": {
"title": "iRobot Roomba",
"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",
"data": {
"host": "Hostname or IP Address",
"blid": "BLID",
"password": "Password",
"certificate": "Certificate",
"continuous": "Continuous",
"delay": "Delay"
}
}
},
"error": {
"unknown": "Unexpected error",
"cannot_connect": "Failed to connect, please try again"
}
},
"options": {
"step": {
"init": {
"data": {
"continuous": "Continuous",
"delay": "Delay"
}
}
}
}
}

View File

@ -1,13 +1,7 @@
"""Support for Wi-Fi enabled iRobot Roombas."""
import asyncio
import logging
import async_timeout
from roomba import Roomba
import voluptuous as vol
from homeassistant.components.vacuum import (
PLATFORM_SCHEMA,
SUPPORT_BATTERY,
SUPPORT_FAN_SPEED,
SUPPORT_LOCATE,
@ -20,9 +14,9 @@ from homeassistant.components.vacuum import (
SUPPORT_TURN_ON,
VacuumDevice,
)
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from . import roomba_reported_state
from .const import BLID, DOMAIN, ROOMBA_SESSION
_LOGGER = logging.getLogger(__name__)
@ -37,34 +31,11 @@ ATTR_SOFTWARE_VERSION = "software_version"
CAP_POSITION = "position"
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_ECO = "Eco"
FAN_SPEED_PERFORMANCE = "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
SUPPORT_ROOMBA = (
@ -83,57 +54,49 @@ SUPPORT_ROOMBA = (
SUPPORT_ROOMBA_CARPET_BOOST = SUPPORT_ROOMBA | SUPPORT_FAN_SPEED
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the iRobot Roomba vacuum cleaner platform."""
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
roomba_vac = RoombaVacuum(name, roomba)
hass.data[PLATFORM][host] = roomba_vac
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the iRobot Roomba vacuum cleaner."""
domain_data = hass.data[DOMAIN][config_entry.entry_id]
roomba = domain_data[ROOMBA_SESSION]
blid = domain_data[BLID]
roomba_vac = RoombaVacuum(roomba, blid)
async_add_entities([roomba_vac], True)
class RoombaVacuum(VacuumDevice):
"""Representation of a Roomba Vacuum cleaner robot."""
def __init__(self, name, roomba):
def __init__(self, roomba, blid):
"""Initialize the Roomba handler."""
self._available = False
self._battery_level = None
self._capabilities = {}
self._fan_speed = None
self._is_on = False
self._name = name
self._state_attrs = {}
self._status = None
self.vacuum = roomba
self.vacuum_state = None
self.vacuum_state = roomba_reported_state(roomba)
self._blid = blid
self._name = self.vacuum_state.get("name")
self._version = self.vacuum_state.get("softwareVer")
self._sku = self.vacuum_state.get("sku")
@property
def unique_id(self):
"""Return the uniqueid of the vacuum cleaner."""
return f"roomba_{self._blid}"
@property
def device_info(self):
"""Return the device info of the vacuum cleaner."""
return {
"identifiers": {(DOMAIN, self.unique_id)},
"manufacturer": "iRobot",
"name": str(self._name),
"sw_version": self._version,
"model": self._sku,
}
@property
def supported_features(self):

View File

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

View File

@ -1815,7 +1815,7 @@ rocketchat-API==0.6.1
roku==4.1.0
# homeassistant.components.roomba
roombapy==1.4.3
roombapy==1.5.0
# homeassistant.components.rova
rova==0.1.0

View File

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

View File

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

View File

@ -0,0 +1,159 @@
"""Test the iRobot Roomba config flow."""
from asynctest import MagicMock, PropertyMock, patch
from roomba import RoombaConnectionError
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.roomba.const import (
CONF_BLID,
CONF_CERT,
CONF_CONTINUOUS,
CONF_DELAY,
DOMAIN,
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD
from tests.common import MockConfigEntry
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_CERT: "/etc/ssl/certs/ca-certificates.crt",
CONF_CONTINUOUS: True,
CONF_DELAY: 1,
}
def _create_mocked_roomba(
roomba_connected=None, master_state=None, connect=None, disconnect=None
):
mocked_roomba = MagicMock()
type(mocked_roomba).roomba_connected = PropertyMock(return_value=roomba_connected)
type(mocked_roomba).master_state = PropertyMock(return_value=master_state)
type(mocked_roomba).connect = MagicMock(side_effect=connect)
type(mocked_roomba).disconnect = MagicMock(side_effect=disconnect)
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}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
mocked_roomba = _create_mocked_roomba(
roomba_connected=True,
master_state={"state": {"reported": {"name": "myroomba"}}},
)
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:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], VALID_CONFIG,
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "myroomba"
assert result2["result"].unique_id == "blid"
assert result2["data"] == {
CONF_BLID: "blid",
CONF_CERT: "/etc/ssl/certs/ca-certificates.crt",
CONF_CONTINUOUS: True,
CONF_DELAY: 1,
CONF_HOST: "1.2.3.4",
CONF_PASSWORD: "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_cannot_connect(hass):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
mocked_roomba = _create_mocked_roomba(
connect=RoombaConnectionError,
roomba_connected=True,
master_state={"state": {"reported": {"name": "myroomba"}}},
)
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,
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_import(hass):
"""Test we can import yaml config."""
mocked_roomba = _create_mocked_roomba(
roomba_connected=True,
master_state={"state": {"reported": {"name": "imported_roomba"}}},
)
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:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=VALID_YAML_CONFIG.copy(),
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["result"].unique_id == "blid"
assert result["title"] == "imported_roomba"
assert result["data"] == {
CONF_BLID: "blid",
CONF_CERT: "/etc/ssl/certs/ca-certificates.crt",
CONF_CONTINUOUS: True,
CONF_DELAY: 1,
CONF_HOST: "1.2.3.4",
CONF_PASSWORD: "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_import_dupe(hass):
"""Test we get abort on duplicate import."""
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(),
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"