diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index f3b150f2c1d..334773c6f86 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except LitterRobotException as ex: raise ConfigEntryNotReady from ex - if hub.account.robots: + if any(hub.litter_robots()): await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -40,6 +40,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] + await hub.account.disconnect() + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/litterrobot/button.py b/homeassistant/components/litterrobot/button.py index 90fbecc6753..5cb65596ec6 100644 --- a/homeassistant/components/litterrobot/button.py +++ b/homeassistant/components/litterrobot/button.py @@ -1,6 +1,8 @@ """Support for Litter-Robot button.""" from __future__ import annotations +from pylitterbot import LitterRobot3 + from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -22,12 +24,11 @@ async def async_setup_entry( """Set up Litter-Robot cleaner using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - LitterRobotResetWasteDrawerButton( - robot=robot, entity_type=TYPE_RESET_WASTE_DRAWER, hub=hub - ) - for robot in hub.account.robots - ] + LitterRobotResetWasteDrawerButton( + robot=robot, entity_type=TYPE_RESET_WASTE_DRAWER, hub=hub + ) + for robot in hub.litter_robots() + if isinstance(robot, LitterRobot3) ) diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 501b71fbd06..b169e075455 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -6,7 +6,7 @@ from datetime import time import logging from typing import Any -from pylitterbot import Robot +from pylitterbot import LitterRobot from pylitterbot.exceptions import InvalidCommandException from typing_extensions import ParamSpec @@ -32,7 +32,9 @@ REFRESH_WAIT_TIME_SECONDS = 8 class LitterRobotEntity(CoordinatorEntity[DataUpdateCoordinator[bool]]): """Generic Litter-Robot entity representing common data and methods.""" - def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: + def __init__( + self, robot: LitterRobot, entity_type: str, hub: LitterRobotHub + ) -> None: """Pass coordinator to CoordinatorEntity.""" super().__init__(hub.coordinator) self.robot = robot @@ -52,6 +54,7 @@ class LitterRobotEntity(CoordinatorEntity[DataUpdateCoordinator[bool]]): @property def device_info(self) -> DeviceInfo: """Return the device information for a Litter-Robot.""" + assert self.robot.serial return DeviceInfo( identifiers={(DOMAIN, self.robot.serial)}, manufacturer="Litter-Robot", @@ -63,7 +66,9 @@ class LitterRobotEntity(CoordinatorEntity[DataUpdateCoordinator[bool]]): class LitterRobotControlEntity(LitterRobotEntity): """A Litter-Robot entity that can control the unit.""" - def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: + def __init__( + self, robot: LitterRobot, entity_type: str, hub: LitterRobotHub + ) -> None: """Init a Litter-Robot control entity.""" super().__init__(robot=robot, entity_type=entity_type, hub=hub) self._refresh_callback: CALLBACK_TYPE | None = None @@ -113,7 +118,7 @@ class LitterRobotControlEntity(LitterRobotEntity): if time_str is None: return None - if (parsed_time := dt_util.parse_time(time_str)) is None: + if (parsed_time := dt_util.parse_time(time_str)) is None: # pragma: no cover return None return ( @@ -132,7 +137,9 @@ class LitterRobotConfigEntity(LitterRobotControlEntity): _attr_entity_category = EntityCategory.CONFIG - def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: + def __init__( + self, robot: LitterRobot, entity_type: str, hub: LitterRobotHub + ) -> None: """Init a Litter-Robot control entity.""" super().__init__(robot=robot, entity_type=entity_type, hub=hub) self._assumed_state: bool | None = None diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index bde4c780482..40ba9e74a7a 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -1,16 +1,17 @@ """A wrapper 'hub' for the Litter-Robot API.""" from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Generator, Mapping from datetime import timedelta import logging from typing import Any -from pylitterbot import Account +from pylitterbot import Account, LitterRobot from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN @@ -23,11 +24,10 @@ UPDATE_INTERVAL_SECONDS = 20 class LitterRobotHub: """A Litter-Robot hub wrapper class.""" - account: Account - def __init__(self, hass: HomeAssistant, data: Mapping[str, Any]) -> None: """Initialize the Litter-Robot hub.""" self._data = data + self.account = Account(websession=async_get_clientsession(hass)) async def _async_update_data() -> bool: """Update all device states from the Litter-Robot API.""" @@ -44,7 +44,6 @@ class LitterRobotHub: async def login(self, load_robots: bool = False) -> None: """Login to Litter-Robot.""" - self.account = Account() try: await self.account.connect( username=self._data[CONF_USERNAME], @@ -58,3 +57,9 @@ class LitterRobotHub: except LitterRobotException as ex: _LOGGER.error("Unable to connect to Litter-Robot API") raise ex + + def litter_robots(self) -> Generator[LitterRobot, Any, Any]: + """Get Litter-Robots from the account.""" + return ( + robot for robot in self.account.robots if isinstance(robot, LitterRobot) + ) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 104a06afc8f..9c5eb1486bf 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==2022.7.0"], + "requirements": ["pylitterbot==2022.8.0"], "codeowners": ["@natekspencer"], "iot_class": "cloud_polling", "loggers": ["pylitterbot"] diff --git a/homeassistant/components/litterrobot/select.py b/homeassistant/components/litterrobot/select.py index c1a0718510d..403f7a8c257 100644 --- a/homeassistant/components/litterrobot/select.py +++ b/homeassistant/components/litterrobot/select.py @@ -1,8 +1,6 @@ """Support for Litter-Robot selects.""" from __future__ import annotations -from pylitterbot.robot import VALID_WAIT_TIMES - from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -24,12 +22,10 @@ async def async_setup_entry( hub: LitterRobotHub = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( - [ - LitterRobotSelect( - robot=robot, entity_type=TYPE_CLEAN_CYCLE_WAIT_TIME_MINUTES, hub=hub - ) - for robot in hub.account.robots - ] + LitterRobotSelect( + robot=robot, entity_type=TYPE_CLEAN_CYCLE_WAIT_TIME_MINUTES, hub=hub + ) + for robot in hub.litter_robots() ) @@ -46,7 +42,7 @@ class LitterRobotSelect(LitterRobotConfigEntity, SelectEntity): @property def options(self) -> list[str]: """Return a set of selectable options.""" - return [str(minute) for minute in VALID_WAIT_TIMES] + return [str(minute) for minute in self.robot.VALID_WAIT_TIMES] async def async_select_option(self, option: str) -> None: """Change the selected option.""" diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index b6dd2a976c3..53ed3605c68 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from datetime import datetime from typing import Any, Union, cast -from pylitterbot.robot import Robot +from pylitterbot import LitterRobot from homeassistant.components.sensor import ( SensorDeviceClass, @@ -40,7 +40,7 @@ class LitterRobotSensorEntityDescription(SensorEntityDescription): """A class that describes Litter-Robot sensor entities.""" icon_fn: Callable[[Any], str | None] = lambda _: None - should_report: Callable[[Robot], bool] = lambda _: True + should_report: Callable[[LitterRobot], bool] = lambda _: True class LitterRobotSensorEntity(LitterRobotEntity, SensorEntity): @@ -50,7 +50,7 @@ class LitterRobotSensorEntity(LitterRobotEntity, SensorEntity): def __init__( self, - robot: Robot, + robot: LitterRobot, hub: LitterRobotHub, description: LitterRobotSensorEntityDescription, ) -> None: @@ -87,13 +87,13 @@ ROBOT_SENSORS = [ name="Sleep Mode Start Time", key="sleep_mode_start_time", device_class=SensorDeviceClass.TIMESTAMP, - should_report=lambda robot: robot.sleep_mode_enabled, # type: ignore[no-any-return] + should_report=lambda robot: robot.sleep_mode_enabled, ), LitterRobotSensorEntityDescription( name="Sleep Mode End Time", key="sleep_mode_end_time", device_class=SensorDeviceClass.TIMESTAMP, - should_report=lambda robot: robot.sleep_mode_enabled, # type: ignore[no-any-return] + should_report=lambda robot: robot.sleep_mode_enabled, ), LitterRobotSensorEntityDescription( name="Last Seen", @@ -120,5 +120,5 @@ async def async_setup_entry( async_add_entities( LitterRobotSensorEntity(robot=robot, hub=hub, description=description) for description in ROBOT_SENSORS - for robot in hub.account.robots + for robot in hub.litter_robots() ) diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 5374add1e34..69050057050 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -21,7 +21,7 @@ class LitterRobotNightLightModeSwitch(LitterRobotConfigEntity, SwitchEntity): """Return true if switch is on.""" if self._refresh_callback is not None: return self._assumed_state - return self.robot.night_light_mode_enabled # type: ignore[no-any-return] + return self.robot.night_light_mode_enabled @property def icon(self) -> str: @@ -45,7 +45,7 @@ class LitterRobotPanelLockoutSwitch(LitterRobotConfigEntity, SwitchEntity): """Return true if switch is on.""" if self._refresh_callback is not None: return self._assumed_state - return self.robot.panel_lock_enabled # type: ignore[no-any-return] + return self.robot.panel_lock_enabled @property def icon(self) -> str: @@ -76,10 +76,8 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot switches using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - - entities: list[SwitchEntity] = [] - for robot in hub.account.robots: - for switch_class, switch_type in ROBOT_SWITCHES: - entities.append(switch_class(robot=robot, entity_type=switch_type, hub=hub)) - - async_add_entities(entities) + async_add_entities( + switch_class(robot=robot, entity_type=switch_type, hub=hub) + for switch_class, switch_type in ROBOT_SWITCHES + for robot in hub.litter_robots() + ) diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 51be573b14e..fe73ac16497 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -5,7 +5,7 @@ import logging from typing import Any from pylitterbot.enums import LitterBoxStatus -from pylitterbot.robot import VALID_WAIT_TIMES +from pylitterbot.robot.litterrobot import VALID_WAIT_TIMES import voluptuous as vol from homeassistant.components.vacuum import ( @@ -56,10 +56,8 @@ async def async_setup_entry( hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - LitterRobotCleaner(robot=robot, entity_type=TYPE_LITTER_BOX, hub=hub) - for robot in hub.account.robots - ] + LitterRobotCleaner(robot=robot, entity_type=TYPE_LITTER_BOX, hub=hub) + for robot in hub.litter_robots() ) platform = entity_platform.async_get_current_platform() diff --git a/requirements_all.txt b/requirements_all.txt index 44c957a9fba..b09a621d4ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1647,7 +1647,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.7.0 +pylitterbot==2022.8.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0b8f21d3f3..6c754ebd0c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1148,7 +1148,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.7.0 +pylitterbot==2022.8.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index 0e3d85dc828..d5d29e12988 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from unittest.mock import AsyncMock, MagicMock, patch -from pylitterbot import Account, Robot +from pylitterbot import Account, LitterRobot3, Robot from pylitterbot.exceptions import InvalidCommandException import pytest @@ -23,7 +23,7 @@ def create_mock_robot( if not robot_data: robot_data = {} - robot = Robot(data={**ROBOT_DATA, **robot_data}) + robot = LitterRobot3(data={**ROBOT_DATA, **robot_data}) robot.start_cleaning = AsyncMock(side_effect=side_effect) robot.set_power_status = AsyncMock(side_effect=side_effect) robot.reset_waste_drawer = AsyncMock(side_effect=side_effect) @@ -31,6 +31,7 @@ def create_mock_robot( robot.set_night_light = AsyncMock(side_effect=side_effect) robot.set_panel_lockout = AsyncMock(side_effect=side_effect) robot.set_wait_time = AsyncMock(side_effect=side_effect) + robot.refresh = AsyncMock(side_effect=side_effect) return robot diff --git a/tests/components/litterrobot/test_select.py b/tests/components/litterrobot/test_select.py index e3e9782423e..eda59216718 100644 --- a/tests/components/litterrobot/test_select.py +++ b/tests/components/litterrobot/test_select.py @@ -1,7 +1,7 @@ """Test the Litter-Robot select entity.""" from datetime import timedelta -from pylitterbot.robot import VALID_WAIT_TIMES +from pylitterbot import LitterRobot3 import pytest from homeassistant.components.litterrobot.entity import REFRESH_WAIT_TIME_SECONDS @@ -38,7 +38,7 @@ async def test_wait_time_select(hass: HomeAssistant, mock_account): data = {ATTR_ENTITY_ID: SELECT_ENTITY_ID} count = 0 - for wait_time in VALID_WAIT_TIMES: + for wait_time in LitterRobot3.VALID_WAIT_TIMES: count += 1 data[ATTR_OPTION] = wait_time