From 47d7a59654ece4171ca40ab056e017a0598327cf Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Tue, 2 Feb 2021 21:49:28 -0700 Subject: [PATCH 1/9] Add litterrobot component and tests --- CODEOWNERS | 1 + .../components/litterrobot/__init__.py | 50 +++++++ .../components/litterrobot/config_flow.py | 69 ++++++++++ homeassistant/components/litterrobot/const.py | 3 + homeassistant/components/litterrobot/hub.py | 125 +++++++++++++++++ .../components/litterrobot/manifest.json | 16 +++ .../components/litterrobot/strings.json | 20 +++ .../litterrobot/translations/en.json | 20 +++ .../components/litterrobot/vacuum.py | 126 ++++++++++++++++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/litterrobot/__init__.py | 1 + tests/components/litterrobot/common.py | 24 ++++ tests/components/litterrobot/conftest.py | 41 ++++++ .../litterrobot/test_config_flow.py | 92 +++++++++++++ tests/components/litterrobot/test_init.py | 18 +++ tests/components/litterrobot/test_vacuum.py | 87 ++++++++++++ 18 files changed, 700 insertions(+) create mode 100644 homeassistant/components/litterrobot/__init__.py create mode 100644 homeassistant/components/litterrobot/config_flow.py create mode 100644 homeassistant/components/litterrobot/const.py create mode 100644 homeassistant/components/litterrobot/hub.py create mode 100644 homeassistant/components/litterrobot/manifest.json create mode 100644 homeassistant/components/litterrobot/strings.json create mode 100644 homeassistant/components/litterrobot/translations/en.json create mode 100644 homeassistant/components/litterrobot/vacuum.py create mode 100644 tests/components/litterrobot/__init__.py create mode 100644 tests/components/litterrobot/common.py create mode 100644 tests/components/litterrobot/conftest.py create mode 100644 tests/components/litterrobot/test_config_flow.py create mode 100644 tests/components/litterrobot/test_init.py create mode 100644 tests/components/litterrobot/test_vacuum.py diff --git a/CODEOWNERS b/CODEOWNERS index a9d4ce63209bce..b20b489ac6dc5a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -253,6 +253,7 @@ homeassistant/components/launch_library/* @ludeeus homeassistant/components/lcn/* @alengwenus homeassistant/components/life360/* @pnbruckner homeassistant/components/linux_battery/* @fabaff +homeassistant/components/litterrobot/* @natekspencer homeassistant/components/local_ip/* @issacg homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py new file mode 100644 index 00000000000000..27e6992485763a --- /dev/null +++ b/homeassistant/components/litterrobot/__init__.py @@ -0,0 +1,50 @@ +"""The Litter-Robot integration.""" +import asyncio + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN +from .hub import LitterRobotHub + +PLATFORMS = ["vacuum"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Litter-Robot component.""" + hass.data.setdefault(DOMAIN, {}) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Litter-Robot from a config entry.""" + hub = hass.data[DOMAIN][entry.entry_id] = LitterRobotHub(hass, entry.data) + try: + await hub.login(load_robots=True) + except Exception as ex: + raise ConfigEntryNotReady from ex + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/litterrobot/config_flow.py b/homeassistant/components/litterrobot/config_flow.py new file mode 100644 index 00000000000000..edf826dd9e5c2a --- /dev/null +++ b/homeassistant/components/litterrobot/config_flow.py @@ -0,0 +1,69 @@ +"""Config flow for Litter-Robot integration.""" +import logging + +from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from .const import DOMAIN # pylint:disable=unused-import +from .hub import LitterRobotHub + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} +) + + +async def validate_input(hass: core.HomeAssistant, data: dict): + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + hub = LitterRobotHub(hass, data) + + try: + await hub.login() + except LitterRobotLoginException as ex: + raise InvalidAuth from ex + except LitterRobotException as ex: + raise CannotConnect from ex + + return {"title": data[CONF_USERNAME]} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Litter-Robot.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + + return self.async_create_entry(title=info["title"], data=user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/litterrobot/const.py b/homeassistant/components/litterrobot/const.py new file mode 100644 index 00000000000000..632465b902d3e2 --- /dev/null +++ b/homeassistant/components/litterrobot/const.py @@ -0,0 +1,3 @@ +"""Constants for the Litter-Robot integration.""" + +DOMAIN = "litterrobot" diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py new file mode 100644 index 00000000000000..fe48f9c716c99d --- /dev/null +++ b/homeassistant/components/litterrobot/hub.py @@ -0,0 +1,125 @@ +"""A wrapper 'hub' for the Litter-Robot API and base entity for common attributes.""" +import asyncio +from datetime import time, timedelta +import logging +from types import MethodType +from typing import Any, Optional + +from pylitterbot import Account, Robot +from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException + +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) +import homeassistant.util.dt as dt_util + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +REFRESH_WAIT_TIME = 12 +UPDATE_INTERVAL = 60 + + +class LitterRobotHub: + """A Litter-Robot hub wrapper class.""" + + def __init__(self, hass: HomeAssistant, data: dict): + """Initialize the Litter-Robot hub.""" + self._data = data + self.account = None + self.logged_in = False + + async def _async_update_data(): + """Update all device states from the Litter-Robot API.""" + await self.account.refresh_robots() + return True + + self.coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN, + update_method=_async_update_data, + update_interval=timedelta(seconds=UPDATE_INTERVAL), + ) + + async def login(self, load_robots: bool = False): + """Login to Litter-Robot.""" + self.logged_in = False + try: + self.account = Account() + await self.account.connect( + username=self._data[CONF_USERNAME], + password=self._data[CONF_PASSWORD], + load_robots=load_robots, + ) + self.logged_in = True + return self.logged_in + except LitterRobotLoginException as ex: + _LOGGER.error("Invalid credentials") + raise ex + except LitterRobotException as ex: + _LOGGER.error("Unable to connect to Litter-Robot API") + raise ex + + +class LitterRobotEntity(CoordinatorEntity): + """Generic Litter-Robot entity representing common data and methods.""" + + def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub): + """Pass coordinator to CoordinatorEntity.""" + super().__init__(hub.coordinator) + self.robot = robot + self.entity_type = entity_type if entity_type else "" + self.hub = hub + + @property + def name(self): + """Return the name of this entity.""" + return f"{self.robot.name} {self.entity_type}" + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self.robot.serial}-{self.entity_type}" + + @property + def available(self): + """Return availability.""" + return self.hub.logged_in + + @property + def device_info(self): + """Return the device information for a Litter-Robot.""" + return { + "identifiers": {(DOMAIN, self.robot.serial)}, + "name": self.robot.name, + "manufacturer": "Litter-Robot", + "model": "Litter-Robot 3 Connect" + if self.robot.serial.startswith("LR3C") + else "Other Litter-Robot Connected Device", + } + + async def perform_action_and_refresh(self, action: MethodType, *args: Any): + """Perform an action and initiates a refresh of the robot data after a few seconds.""" + await action(*args) + await asyncio.sleep(REFRESH_WAIT_TIME) + await self.hub.coordinator.async_refresh() + + @staticmethod + def parse_time_at_default_timezone(time_str: str) -> Optional[time]: + """Parse a time string and add default timezone.""" + parsed_time = dt_util.parse_time(time_str) + return ( + None + if parsed_time is None + else time( + hour=parsed_time.hour, + minute=parsed_time.minute, + second=parsed_time.second, + tzinfo=dt_util.DEFAULT_TIME_ZONE, + ) + ) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json new file mode 100644 index 00000000000000..b2c7831f552156 --- /dev/null +++ b/homeassistant/components/litterrobot/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "litterrobot", + "name": "Litter-Robot", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/litterrobot", + "requirements": [ + "pylitterbot==2021.2.2" + ], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": [ + "@natekspencer" + ] +} diff --git a/homeassistant/components/litterrobot/strings.json b/homeassistant/components/litterrobot/strings.json new file mode 100644 index 00000000000000..96dc8b371d143c --- /dev/null +++ b/homeassistant/components/litterrobot/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/litterrobot/translations/en.json b/homeassistant/components/litterrobot/translations/en.json new file mode 100644 index 00000000000000..b3fc76ae458f25 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + } + } + } + } +} diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py new file mode 100644 index 00000000000000..17ed25423548ef --- /dev/null +++ b/homeassistant/components/litterrobot/vacuum.py @@ -0,0 +1,126 @@ +"""Support for Litter-Robot "Vacuum".""" +from pylitterbot import Robot + +from homeassistant.components.vacuum import ( + STATE_CLEANING, + STATE_DOCKED, + STATE_ERROR, + SUPPORT_SEND_COMMAND, + SUPPORT_START, + SUPPORT_STATE, + SUPPORT_STATUS, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + VacuumEntity, +) +from homeassistant.const import STATE_OFF + +from .const import DOMAIN +from .hub import LitterRobotEntity + +SUPPORT_LITTERROBOT = ( + SUPPORT_SEND_COMMAND + | SUPPORT_START + | SUPPORT_STATE + | SUPPORT_STATUS + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON +) +TYPE_LITTER_BOX = "Litter Box" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Litter-Robot cleaner using config entry.""" + hub = hass.data[DOMAIN][config_entry.entry_id] + + entities = [] + for robot in hub.account.robots: + entities.append(LitterRobotCleaner(robot, TYPE_LITTER_BOX, hub)) + + if entities: + async_add_entities(entities, True) + + +class LitterRobotCleaner(LitterRobotEntity, VacuumEntity): + """Litter-Robot "Vacuum" Cleaner.""" + + @property + def supported_features(self): + """Flag cleaner robot features that are supported.""" + return SUPPORT_LITTERROBOT + + @property + def state(self): + """Return the state of the cleaner.""" + switcher = { + Robot.UnitStatus.CCP: STATE_CLEANING, + Robot.UnitStatus.EC: STATE_CLEANING, + Robot.UnitStatus.CCC: STATE_DOCKED, + Robot.UnitStatus.CST: STATE_DOCKED, + Robot.UnitStatus.DF1: STATE_DOCKED, + Robot.UnitStatus.DF2: STATE_DOCKED, + Robot.UnitStatus.RDY: STATE_DOCKED, + Robot.UnitStatus.OFF: STATE_OFF, + } + + return switcher.get(self.robot.unit_status, STATE_ERROR) + + @property + def error(self): + """Return the error associated with the current state, if any.""" + return self.robot.unit_status.value + + @property + def status(self): + """Return the status of the cleaner.""" + return f"{self.robot.unit_status.value}{' (Sleeping)' if self.robot.is_sleeping else ''}" + + async def async_turn_on(self, **kwargs): + """Turn the cleaner on, starting a clean cycle.""" + await self.perform_action_and_refresh(self.robot.set_power_status, True) + + async def async_turn_off(self, **kwargs): + """Turn the unit off, stopping any cleaning in progress as is.""" + await self.perform_action_and_refresh(self.robot.set_power_status, False) + + async def async_start(self): + """Start a clean cycle.""" + await self.perform_action_and_refresh(self.robot.start_cleaning) + + async def async_send_command(self, command, params=None, **kwargs): + """Send command. + + Available commands: + - reset_waste_drawer + * params: none + - set_sleep_mode + * params: + - enabled: bool + - sleep_time: str (optional) + + """ + if command == "reset_waste_drawer": + # Normally we need to request a refresh of data after a command is sent. + # However, the API for resetting the waste drawer returns a refreshed + # data set for the robot. Thus, we only need to tell hass to update the + # state of devices associated with this robot. + await self.robot.reset_waste_drawer() + self.hub.coordinator.async_set_updated_data(True) + elif command == "set_sleep_mode": + await self.perform_action_and_refresh( + self.robot.set_sleep_mode, + params.get("enabled"), + self.parse_time_at_default_timezone(params.get("sleep_time")), + ) + else: + raise NotImplementedError() + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + return { + "clean_cycle_wait_time_minutes": self.robot.clean_cycle_wait_time_minutes, + "is_sleeping": self.robot.is_sleeping, + "power_status": self.robot.power_status, + "last_seen": self.robot.last_seen, + } diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 8e8949e5788c47..36c22262ef4d86 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -121,6 +121,7 @@ "kulersky", "life360", "lifx", + "litterrobot", "local_ip", "locative", "logi_circle", diff --git a/requirements_all.txt b/requirements_all.txt index b98cbde46bca68..806ec9faf40ce6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1503,6 +1503,9 @@ pylibrespot-java==0.1.0 # homeassistant.components.litejet pylitejet==0.1 +# homeassistant.components.litterrobot +pylitterbot==2021.2.2 + # homeassistant.components.loopenergy pyloopenergy==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1beff34323421..18b0e89c5f196b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -793,6 +793,9 @@ pylibrespot-java==0.1.0 # homeassistant.components.litejet pylitejet==0.1 +# homeassistant.components.litterrobot +pylitterbot==2021.2.2 + # homeassistant.components.lutron_caseta pylutron-caseta==0.9.0 diff --git a/tests/components/litterrobot/__init__.py b/tests/components/litterrobot/__init__.py new file mode 100644 index 00000000000000..a726736510035e --- /dev/null +++ b/tests/components/litterrobot/__init__.py @@ -0,0 +1 @@ +"""Tests for the Litter-Robot Component.""" diff --git a/tests/components/litterrobot/common.py b/tests/components/litterrobot/common.py new file mode 100644 index 00000000000000..ed893a3a756865 --- /dev/null +++ b/tests/components/litterrobot/common.py @@ -0,0 +1,24 @@ +"""Common utils for Litter-Robot tests.""" +from homeassistant.components.litterrobot import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +BASE_PATH = "homeassistant.components.litterrobot" +CONFIG = {DOMAIN: {CONF_USERNAME: "user@example.com", CONF_PASSWORD: "password"}} + +ROBOT_NAME = "Test" +ROBOT_SERIAL = "LR3C012345" +ROBOT_DATA = { + "powerStatus": "AC", + "lastSeen": "2021-02-01T15:30:00.000000", + "cleanCycleWaitTimeMinutes": "7", + "unitStatus": "RDY", + "litterRobotNickname": ROBOT_NAME, + "cycleCount": "15", + "panelLockActive": "0", + "cyclesAfterDrawerFull": "0", + "litterRobotSerial": ROBOT_SERIAL, + "cycleCapacity": "30", + "litterRobotId": "a0123b4567cd8e", + "nightLightActive": "1", + "sleepModeActive": "112:50:19", +} diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py new file mode 100644 index 00000000000000..60a867146bac4e --- /dev/null +++ b/tests/components/litterrobot/conftest.py @@ -0,0 +1,41 @@ +"""Configure pytest for Litter-Robot tests.""" +from unittest.mock import AsyncMock, MagicMock, patch + +from pylitterbot import Robot +import pytest + +from homeassistant.components import litterrobot +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .common import BASE_PATH, ROBOT_DATA + + +@pytest.fixture(autouse=True) +def no_refresh_wait_time(): + """Make the refresh wait tiime 0 for instant tests.""" + with patch(f"{BASE_PATH}.hub.REFRESH_WAIT_TIME", 0): + yield + + +def create_mock_robot(hass): + """Create a mock Litter-Robot device.""" + robot = Robot(data=ROBOT_DATA) + robot.start_cleaning = AsyncMock() + robot.set_power_status = AsyncMock() + robot.reset_waste_drawer = AsyncMock() + robot.set_sleep_mode = AsyncMock() + return robot + + +@pytest.fixture() +def mock_hub(hass): + """Mock a Litter-Robot hub.""" + hub = MagicMock( + hass=hass, + account=MagicMock(), + logged_in=True, + coordinator=MagicMock(spec=DataUpdateCoordinator), + spec=litterrobot.LitterRobotHub, + ) + hub.account.robots = [create_mock_robot(hass)] + return hub diff --git a/tests/components/litterrobot/test_config_flow.py b/tests/components/litterrobot/test_config_flow.py new file mode 100644 index 00000000000000..fd88595d37e19e --- /dev/null +++ b/tests/components/litterrobot/test_config_flow.py @@ -0,0 +1,92 @@ +"""Test the Litter-Robot config flow.""" +from unittest.mock import patch + +from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException + +from homeassistant import config_entries, setup + +from .common import CONF_USERNAME, CONFIG, 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.litterrobot.config_flow.LitterRobotHub.login", + return_value=True, + ), patch( + "homeassistant.components.litterrobot.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.litterrobot.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG[DOMAIN] + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == CONFIG[DOMAIN][CONF_USERNAME] + assert result2["data"] == CONFIG[DOMAIN] + 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.litterrobot.config_flow.LitterRobotHub.login", + side_effect=LitterRobotLoginException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG[DOMAIN] + ) + + 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.litterrobot.config_flow.LitterRobotHub.login", + side_effect=LitterRobotException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG[DOMAIN] + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unknown_error(hass): + """Test we handle unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.litterrobot.config_flow.LitterRobotHub.login", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG[DOMAIN] + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/litterrobot/test_init.py b/tests/components/litterrobot/test_init.py new file mode 100644 index 00000000000000..9e16e8d64e053a --- /dev/null +++ b/tests/components/litterrobot/test_init.py @@ -0,0 +1,18 @@ +"""Test Litter-Robot setup process.""" +from homeassistant.components import litterrobot +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_unload_entry(hass): + """Test being able to unload an entry.""" + entry = MockConfigEntry( + domain=litterrobot.DOMAIN, + data={"username": "test-username", "password": "test-password"}, + ) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, litterrobot.DOMAIN, {}) is True + assert await litterrobot.async_unload_entry(hass, entry) + assert hass.data[litterrobot.DOMAIN] == {} diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py new file mode 100644 index 00000000000000..c8146774ad12b2 --- /dev/null +++ b/tests/components/litterrobot/test_vacuum.py @@ -0,0 +1,87 @@ +"""Test the Litter-Robot vacuum entity.""" +import pytest + +from homeassistant import config_entries +from homeassistant.components import litterrobot +from homeassistant.components.vacuum import ( + ATTR_PARAMS, + DOMAIN as PLATFORM_DOMAIN, + SERVICE_SEND_COMMAND, + SERVICE_START, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_DOCKED, +) +from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID + +ENTITY_ID = "vacuum.test_litter_box" + + +async def setup_hub(hass, mock_hub): + """Load the Litter-Robot vacuum platform with the provided hub.""" + hass.config.components.add(litterrobot.DOMAIN) + config_entry = config_entries.ConfigEntry( + 1, + litterrobot.DOMAIN, + "Mock Title", + {"host": "mock-host"}, + "test", + config_entries.CONN_CLASS_LOCAL_POLL, + system_options={}, + ) + mock_hub.config_entry = config_entry + hass.data[litterrobot.DOMAIN] = {config_entry.entry_id: mock_hub} + await hass.config_entries.async_forward_entry_setup(config_entry, PLATFORM_DOMAIN) + await hass.async_block_till_done() + + +async def test_vacuum(hass, mock_hub): + """Tests the vacuum entity was set up.""" + await setup_hub(hass, mock_hub) + + vacuum = hass.states.get(ENTITY_ID) + assert vacuum is not None + assert vacuum.state == STATE_DOCKED + assert vacuum.attributes["is_sleeping"] is False + + +@pytest.mark.parametrize( + "service,command,extra", + [ + (SERVICE_START, "start_cleaning", None), + (SERVICE_TURN_OFF, "set_power_status", None), + (SERVICE_TURN_ON, "set_power_status", None), + ( + SERVICE_SEND_COMMAND, + "reset_waste_drawer", + {ATTR_COMMAND: "reset_waste_drawer"}, + ), + ( + SERVICE_SEND_COMMAND, + "set_sleep_mode", + { + ATTR_COMMAND: "set_sleep_mode", + ATTR_PARAMS: {"enabled": True, "sleep_time": "22:30"}, + }, + ), + ], +) +async def test_commands(hass, mock_hub, service, command, extra): + """Test sending commands to the vacuum.""" + await setup_hub(hass, mock_hub) + + vacuum = hass.states.get(ENTITY_ID) + assert vacuum is not None + assert vacuum.state == STATE_DOCKED + + data = {ATTR_ENTITY_ID: ENTITY_ID} + if extra: + data.update(extra) + + await hass.services.async_call( + PLATFORM_DOMAIN, + service, + data, + blocking=True, + ) + getattr(mock_hub.account.robots[0], command).assert_called_once() From dcdf5bad8d2ce5e7e96a84197fa4490e87e9598c Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Wed, 3 Feb 2021 08:01:24 +0000 Subject: [PATCH 2/9] Add constraint in config_flow to not allow integration if it has already been configured for a username --- homeassistant/components/litterrobot/config_flow.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/litterrobot/config_flow.py b/homeassistant/components/litterrobot/config_flow.py index edf826dd9e5c2a..cad7792aa1f432 100644 --- a/homeassistant/components/litterrobot/config_flow.py +++ b/homeassistant/components/litterrobot/config_flow.py @@ -43,7 +43,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} + if user_input is not None: + for entry in self._async_current_entries(): + if entry.data[CONF_USERNAME] == user_input[CONF_USERNAME]: + return self.async_abort(reason="already_configured") + try: info = await validate_input(self.hass, user_input) From 194ec8bf6e04c483877a724fc31c01bb9f35f443 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Fri, 5 Feb 2021 07:26:45 +0000 Subject: [PATCH 3/9] update tests to use MockConfigEntry --- tests/components/litterrobot/test_vacuum.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index c8146774ad12b2..e5a8cc464a6309 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -1,7 +1,6 @@ """Test the Litter-Robot vacuum entity.""" import pytest -from homeassistant import config_entries from homeassistant.components import litterrobot from homeassistant.components.vacuum import ( ATTR_PARAMS, @@ -14,24 +13,22 @@ ) from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID +from .common import CONFIG + +from tests.common import MockConfigEntry + ENTITY_ID = "vacuum.test_litter_box" async def setup_hub(hass, mock_hub): """Load the Litter-Robot vacuum platform with the provided hub.""" hass.config.components.add(litterrobot.DOMAIN) - config_entry = config_entries.ConfigEntry( - 1, - litterrobot.DOMAIN, - "Mock Title", - {"host": "mock-host"}, - "test", - config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, + entry = MockConfigEntry( + domain=litterrobot.DOMAIN, + data=CONFIG[litterrobot.DOMAIN], ) - mock_hub.config_entry = config_entry - hass.data[litterrobot.DOMAIN] = {config_entry.entry_id: mock_hub} - await hass.config_entries.async_forward_entry_setup(config_entry, PLATFORM_DOMAIN) + hass.data[litterrobot.DOMAIN] = {entry.entry_id: mock_hub} + await hass.config_entries.async_forward_entry_setup(entry, PLATFORM_DOMAIN) await hass.async_block_till_done() From 2d697b7254fd2c0dfd73c7e5da96338e54b87b9b Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Thu, 11 Feb 2021 18:10:58 +0000 Subject: [PATCH 4/9] Use pylitterbot 2021.2.3 --- homeassistant/components/litterrobot/manifest.json | 8 ++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index b2c7831f552156..d191e52c605e10 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,14 +3,10 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": [ - "pylitterbot==2021.2.2" - ], + "requirements": ["pylitterbot==2021.2.3"], "ssdp": [], "zeroconf": [], "homekit": {}, "dependencies": [], - "codeowners": [ - "@natekspencer" - ] + "codeowners": ["@natekspencer"] } diff --git a/requirements_all.txt b/requirements_all.txt index 806ec9faf40ce6..919a94ba3441ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1504,7 +1504,7 @@ pylibrespot-java==0.1.0 pylitejet==0.1 # homeassistant.components.litterrobot -pylitterbot==2021.2.2 +pylitterbot==2021.2.3 # homeassistant.components.loopenergy pyloopenergy==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18b0e89c5f196b..45688ed85f912f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -794,7 +794,7 @@ pylibrespot-java==0.1.0 pylitejet==0.1 # homeassistant.components.litterrobot -pylitterbot==2021.2.2 +pylitterbot==2021.2.3 # homeassistant.components.lutron_caseta pylutron-caseta==0.9.0 From 3720ad029aa9ea8dea4dfdc6d87a3bc2b2624221 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Fri, 19 Feb 2021 19:48:49 +0000 Subject: [PATCH 5/9] Bump pylitterbot to 2021.2.5 --- homeassistant/components/litterrobot/const.py | 1 - homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/litterrobot/const.py b/homeassistant/components/litterrobot/const.py index 632465b902d3e2..5ac889d9b738a9 100644 --- a/homeassistant/components/litterrobot/const.py +++ b/homeassistant/components/litterrobot/const.py @@ -1,3 +1,2 @@ """Constants for the Litter-Robot integration.""" - DOMAIN = "litterrobot" diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index d191e52c605e10..77b5e8f85132d7 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,7 +3,7 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2021.2.3"], + "requirements": ["pylitterbot==2021.2.5"], "ssdp": [], "zeroconf": [], "homekit": {}, diff --git a/requirements_all.txt b/requirements_all.txt index 919a94ba3441ec..55ea1510b59830 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1504,7 +1504,7 @@ pylibrespot-java==0.1.0 pylitejet==0.1 # homeassistant.components.litterrobot -pylitterbot==2021.2.3 +pylitterbot==2021.2.5 # homeassistant.components.loopenergy pyloopenergy==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45688ed85f912f..b590a8c50b3b5b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -794,7 +794,7 @@ pylibrespot-java==0.1.0 pylitejet==0.1 # homeassistant.components.litterrobot -pylitterbot==2021.2.3 +pylitterbot==2021.2.5 # homeassistant.components.lutron_caseta pylutron-caseta==0.9.0 From ea99c3e3d1ace13fd4dc60ab643425cb5ce65f6e Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sat, 20 Feb 2021 15:44:09 -0700 Subject: [PATCH 6/9] Apply suggestions from code review Co-authored-by: J. Nick Koston --- .../components/litterrobot/config_flow.py | 19 +++---------------- .../components/litterrobot/manifest.json | 4 ---- tests/components/litterrobot/conftest.py | 2 +- 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/litterrobot/config_flow.py b/homeassistant/components/litterrobot/config_flow.py index cad7792aa1f432..0375edf716a578 100644 --- a/homeassistant/components/litterrobot/config_flow.py +++ b/homeassistant/components/litterrobot/config_flow.py @@ -24,12 +24,7 @@ async def validate_input(hass: core.HomeAssistant, data: dict): """ hub = LitterRobotHub(hass, data) - try: - await hub.login() - except LitterRobotLoginException as ex: - raise InvalidAuth from ex - except LitterRobotException as ex: - raise CannotConnect from ex + await hub.login() return {"title": data[CONF_USERNAME]} @@ -53,9 +48,9 @@ async def async_step_user(self, user_input=None): info = await validate_input(self.hass, user_input) return self.async_create_entry(title=info["title"], data=user_input) - except CannotConnect: + except LitterRobotException: errors["base"] = "cannot_connect" - except InvalidAuth: + except LitterRobotLoginException: errors["base"] = "invalid_auth" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") @@ -64,11 +59,3 @@ async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - - -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" - - -class InvalidAuth(exceptions.HomeAssistantError): - """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 77b5e8f85132d7..1c6ac7274bf657 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -4,9 +4,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", "requirements": ["pylitterbot==2021.2.5"], - "ssdp": [], - "zeroconf": [], - "homekit": {}, - "dependencies": [], "codeowners": ["@natekspencer"] } diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index 60a867146bac4e..3c634204eb5085 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -12,7 +12,7 @@ @pytest.fixture(autouse=True) def no_refresh_wait_time(): - """Make the refresh wait tiime 0 for instant tests.""" + """Make the refresh wait time 0 for instant tests.""" with patch(f"{BASE_PATH}.hub.REFRESH_WAIT_TIME", 0): yield From 6d9449eaf736b0b59ee05245ed47f9d87ec996c6 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sun, 21 Feb 2021 23:19:51 +0000 Subject: [PATCH 7/9] Adjust code based on code review --- .../components/litterrobot/__init__.py | 6 ++- .../components/litterrobot/config_flow.py | 6 +-- homeassistant/components/litterrobot/hub.py | 39 +++++++++---------- tests/components/litterrobot/conftest.py | 12 ++---- tests/components/litterrobot/test_init.py | 4 +- tests/components/litterrobot/test_vacuum.py | 16 ++++++-- 6 files changed, 44 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 27e6992485763a..bf43d5c465eeda 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -1,6 +1,8 @@ """The Litter-Robot integration.""" import asyncio +from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -23,7 +25,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hub = hass.data[DOMAIN][entry.entry_id] = LitterRobotHub(hass, entry.data) try: await hub.login(load_robots=True) - except Exception as ex: + except LitterRobotLoginException: + return False + except LitterRobotException as ex: raise ConfigEntryNotReady from ex for component in PLATFORMS: diff --git a/homeassistant/components/litterrobot/config_flow.py b/homeassistant/components/litterrobot/config_flow.py index 0375edf716a578..0641769d9ccac3 100644 --- a/homeassistant/components/litterrobot/config_flow.py +++ b/homeassistant/components/litterrobot/config_flow.py @@ -4,7 +4,7 @@ from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries, core from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import DOMAIN # pylint:disable=unused-import @@ -48,10 +48,10 @@ async def async_step_user(self, user_input=None): info = await validate_input(self.hass, user_input) return self.async_create_entry(title=info["title"], data=user_input) - except LitterRobotException: - errors["base"] = "cannot_connect" except LitterRobotLoginException: errors["base"] = "invalid_auth" + except LitterRobotException: + errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index fe48f9c716c99d..00f56018410b1c 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -1,5 +1,4 @@ """A wrapper 'hub' for the Litter-Robot API and base entity for common attributes.""" -import asyncio from datetime import time, timedelta import logging from types import MethodType @@ -10,6 +9,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -49,8 +49,8 @@ async def _async_update_data(): async def login(self, load_robots: bool = False): """Login to Litter-Robot.""" self.logged_in = False + self.account = Account() try: - self.account = Account() await self.account.connect( username=self._data[CONF_USERNAME], password=self._data[CONF_PASSWORD], @@ -86,40 +86,37 @@ def unique_id(self): """Return a unique ID.""" return f"{self.robot.serial}-{self.entity_type}" - @property - def available(self): - """Return availability.""" - return self.hub.logged_in - @property def device_info(self): """Return the device information for a Litter-Robot.""" + model = "Litter-Robot 3 Connect" + if not self.robot.serial.startswith("LR3C"): + model = "Other Litter-Robot Connected Device" return { "identifiers": {(DOMAIN, self.robot.serial)}, "name": self.robot.name, "manufacturer": "Litter-Robot", - "model": "Litter-Robot 3 Connect" - if self.robot.serial.startswith("LR3C") - else "Other Litter-Robot Connected Device", + "model": model, } async def perform_action_and_refresh(self, action: MethodType, *args: Any): """Perform an action and initiates a refresh of the robot data after a few seconds.""" await action(*args) - await asyncio.sleep(REFRESH_WAIT_TIME) - await self.hub.coordinator.async_refresh() + async_call_later( + self.hass, REFRESH_WAIT_TIME, self.hub.coordinator.async_request_refresh + ) @staticmethod def parse_time_at_default_timezone(time_str: str) -> Optional[time]: """Parse a time string and add default timezone.""" parsed_time = dt_util.parse_time(time_str) - return ( - None - if parsed_time is None - else time( - hour=parsed_time.hour, - minute=parsed_time.minute, - second=parsed_time.second, - tzinfo=dt_util.DEFAULT_TIME_ZONE, - ) + + if parsed_time is None: + return None + + return time( + hour=parsed_time.hour, + minute=parsed_time.minute, + second=parsed_time.second, + tzinfo=dt_util.DEFAULT_TIME_ZONE, ) diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index 3c634204eb5085..2f967d266bc24d 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -1,5 +1,5 @@ """Configure pytest for Litter-Robot tests.""" -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock from pylitterbot import Robot import pytest @@ -7,14 +7,7 @@ from homeassistant.components import litterrobot from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .common import BASE_PATH, ROBOT_DATA - - -@pytest.fixture(autouse=True) -def no_refresh_wait_time(): - """Make the refresh wait time 0 for instant tests.""" - with patch(f"{BASE_PATH}.hub.REFRESH_WAIT_TIME", 0): - yield +from .common import ROBOT_DATA def create_mock_robot(hass): @@ -37,5 +30,6 @@ def mock_hub(hass): coordinator=MagicMock(spec=DataUpdateCoordinator), spec=litterrobot.LitterRobotHub, ) + hub.coordinator.last_update_success = True hub.account.robots = [create_mock_robot(hass)] return hub diff --git a/tests/components/litterrobot/test_init.py b/tests/components/litterrobot/test_init.py index 9e16e8d64e053a..1d0ed075cc77e7 100644 --- a/tests/components/litterrobot/test_init.py +++ b/tests/components/litterrobot/test_init.py @@ -2,6 +2,8 @@ from homeassistant.components import litterrobot from homeassistant.setup import async_setup_component +from .common import CONFIG + from tests.common import MockConfigEntry @@ -9,7 +11,7 @@ async def test_unload_entry(hass): """Test being able to unload an entry.""" entry = MockConfigEntry( domain=litterrobot.DOMAIN, - data={"username": "test-username", "password": "test-password"}, + data=CONFIG[litterrobot.DOMAIN], ) entry.add_to_hass(hass) diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index e5a8cc464a6309..b47eff64e136e6 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -1,7 +1,11 @@ """Test the Litter-Robot vacuum entity.""" +from datetime import timedelta +from unittest.mock import patch + import pytest from homeassistant.components import litterrobot +from homeassistant.components.litterrobot.hub import REFRESH_WAIT_TIME from homeassistant.components.vacuum import ( ATTR_PARAMS, DOMAIN as PLATFORM_DOMAIN, @@ -12,10 +16,11 @@ STATE_DOCKED, ) from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID +from homeassistant.util.dt import utcnow from .common import CONFIG -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed ENTITY_ID = "vacuum.test_litter_box" @@ -27,9 +32,10 @@ async def setup_hub(hass, mock_hub): domain=litterrobot.DOMAIN, data=CONFIG[litterrobot.DOMAIN], ) - hass.data[litterrobot.DOMAIN] = {entry.entry_id: mock_hub} - await hass.config_entries.async_forward_entry_setup(entry, PLATFORM_DOMAIN) - await hass.async_block_till_done() + + with patch.dict(hass.data, {litterrobot.DOMAIN: {entry.entry_id: mock_hub}}): + await hass.config_entries.async_forward_entry_setup(entry, PLATFORM_DOMAIN) + await hass.async_block_till_done() async def test_vacuum(hass, mock_hub): @@ -81,4 +87,6 @@ async def test_commands(hass, mock_hub, service, command, extra): data, blocking=True, ) + future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME) + async_fire_time_changed(hass, future) getattr(mock_hub.account.robots[0], command).assert_called_once() From 094bc908dfdb14313712c8d935cc1c57ebd09855 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 22 Feb 2021 17:33:57 +0000 Subject: [PATCH 8/9] Add unit_status_code to vacuum, reduce update_interval for hub and remove unnecessary method in config_flow --- .../components/litterrobot/config_flow.py | 24 ++++++------------- homeassistant/components/litterrobot/hub.py | 2 +- .../components/litterrobot/vacuum.py | 1 + 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/litterrobot/config_flow.py b/homeassistant/components/litterrobot/config_flow.py index 0641769d9ccac3..8a94c9d75ae4d8 100644 --- a/homeassistant/components/litterrobot/config_flow.py +++ b/homeassistant/components/litterrobot/config_flow.py @@ -4,10 +4,10 @@ from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException import voluptuous as vol -from homeassistant import config_entries, core +from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import DOMAIN # pylint:disable=unused-import +from .const import DOMAIN from .hub import LitterRobotHub _LOGGER = logging.getLogger(__name__) @@ -17,18 +17,6 @@ ) -async def validate_input(hass: core.HomeAssistant, data: dict): - """Validate the user input allows us to connect. - - Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. - """ - hub = LitterRobotHub(hass, data) - - await hub.login() - - return {"title": data[CONF_USERNAME]} - - class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Litter-Robot.""" @@ -44,10 +32,12 @@ async def async_step_user(self, user_input=None): if entry.data[CONF_USERNAME] == user_input[CONF_USERNAME]: return self.async_abort(reason="already_configured") + hub = LitterRobotHub(self.hass, user_input) try: - info = await validate_input(self.hass, user_input) - - return self.async_create_entry(title=info["title"], data=user_input) + await hub.login() + return self.async_create_entry( + title=user_input[CONF_USERNAME], data=user_input + ) except LitterRobotLoginException: errors["base"] = "invalid_auth" except LitterRobotException: diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index 00f56018410b1c..0d0559140c7431 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__) REFRESH_WAIT_TIME = 12 -UPDATE_INTERVAL = 60 +UPDATE_INTERVAL = 10 class LitterRobotHub: diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 17ed25423548ef..a57c1ffead513e 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -122,5 +122,6 @@ def device_state_attributes(self): "clean_cycle_wait_time_minutes": self.robot.clean_cycle_wait_time_minutes, "is_sleeping": self.robot.is_sleeping, "power_status": self.robot.power_status, + "unit_status_code": self.robot.unit_status.name, "last_seen": self.robot.last_seen, } From 8099fad589e208e8a17cabdf264aef90ebe5cfd3 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 22 Feb 2021 18:17:45 +0000 Subject: [PATCH 9/9] Fix pylint error... --- homeassistant/components/litterrobot/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/litterrobot/config_flow.py b/homeassistant/components/litterrobot/config_flow.py index 8a94c9d75ae4d8..d6c92d8dad64b3 100644 --- a/homeassistant/components/litterrobot/config_flow.py +++ b/homeassistant/components/litterrobot/config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import DOMAIN +from .const import DOMAIN # pylint:disable=unused-import from .hub import LitterRobotHub _LOGGER = logging.getLogger(__name__)