mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
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:
parent
d5f73378f0
commit
8c4a139aeb
@ -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
|
||||
|
33
homeassistant/components/roomba/.translations/en.json
Normal file
33
homeassistant/components/roomba/.translations/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
homeassistant/components/roomba/.translations/fr.json
Normal file
33
homeassistant/components/roomba/.translations/fr.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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."""
|
||||
|
74
homeassistant/components/roomba/binary_sensor.py
Normal file
74
homeassistant/components/roomba/binary_sensor.py
Normal 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)
|
131
homeassistant/components/roomba/config_flow.py
Normal file
131
homeassistant/components/roomba/config_flow.py
Normal 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,
|
||||
}
|
||||
),
|
||||
)
|
13
homeassistant/components/roomba/const.py
Normal file
13
homeassistant/components/roomba/const.py
Normal 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"
|
@ -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"]
|
||||
}
|
||||
|
76
homeassistant/components/roomba/sensor.py
Normal file
76
homeassistant/components/roomba/sensor.py
Normal 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
|
||||
)
|
33
homeassistant/components/roomba/strings.json
Normal file
33
homeassistant/components/roomba/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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):
|
||||
|
@ -97,6 +97,7 @@ FLOWS = [
|
||||
"rainmachine",
|
||||
"ring",
|
||||
"roku",
|
||||
"roomba",
|
||||
"samsungtv",
|
||||
"sense",
|
||||
"sentry",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
1
tests/components/roomba/__init__.py
Normal file
1
tests/components/roomba/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the iRobot Roomba integration."""
|
159
tests/components/roomba/test_config_flow.py
Normal file
159
tests/components/roomba/test_config_flow.py
Normal 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"
|
Loading…
x
Reference in New Issue
Block a user