Add support for Litter-Robot 4 (#75790)

This commit is contained in:
Nathan Spencer 2022-08-25 10:32:27 -06:00 committed by GitHub
parent 462ec4ced3
commit b563bd0ae5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 63 additions and 53 deletions

View file

@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except LitterRobotException as ex: except LitterRobotException as ex:
raise ConfigEntryNotReady from ex raise ConfigEntryNotReady from ex
if hub.account.robots: if any(hub.litter_robots()):
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True 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: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) 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: if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)

View file

@ -1,6 +1,8 @@
"""Support for Litter-Robot button.""" """Support for Litter-Robot button."""
from __future__ import annotations from __future__ import annotations
from pylitterbot import LitterRobot3
from homeassistant.components.button import ButtonEntity from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -22,12 +24,11 @@ async def async_setup_entry(
"""Set up Litter-Robot cleaner using config entry.""" """Set up Litter-Robot cleaner using config entry."""
hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id]
async_add_entities( async_add_entities(
[ LitterRobotResetWasteDrawerButton(
LitterRobotResetWasteDrawerButton( robot=robot, entity_type=TYPE_RESET_WASTE_DRAWER, hub=hub
robot=robot, entity_type=TYPE_RESET_WASTE_DRAWER, hub=hub )
) for robot in hub.litter_robots()
for robot in hub.account.robots if isinstance(robot, LitterRobot3)
]
) )

View file

@ -6,7 +6,7 @@ from datetime import time
import logging import logging
from typing import Any from typing import Any
from pylitterbot import Robot from pylitterbot import LitterRobot
from pylitterbot.exceptions import InvalidCommandException from pylitterbot.exceptions import InvalidCommandException
from typing_extensions import ParamSpec from typing_extensions import ParamSpec
@ -32,7 +32,9 @@ REFRESH_WAIT_TIME_SECONDS = 8
class LitterRobotEntity(CoordinatorEntity[DataUpdateCoordinator[bool]]): class LitterRobotEntity(CoordinatorEntity[DataUpdateCoordinator[bool]]):
"""Generic Litter-Robot entity representing common data and methods.""" """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.""" """Pass coordinator to CoordinatorEntity."""
super().__init__(hub.coordinator) super().__init__(hub.coordinator)
self.robot = robot self.robot = robot
@ -52,6 +54,7 @@ class LitterRobotEntity(CoordinatorEntity[DataUpdateCoordinator[bool]]):
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return the device information for a Litter-Robot.""" """Return the device information for a Litter-Robot."""
assert self.robot.serial
return DeviceInfo( return DeviceInfo(
identifiers={(DOMAIN, self.robot.serial)}, identifiers={(DOMAIN, self.robot.serial)},
manufacturer="Litter-Robot", manufacturer="Litter-Robot",
@ -63,7 +66,9 @@ class LitterRobotEntity(CoordinatorEntity[DataUpdateCoordinator[bool]]):
class LitterRobotControlEntity(LitterRobotEntity): class LitterRobotControlEntity(LitterRobotEntity):
"""A Litter-Robot entity that can control the unit.""" """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.""" """Init a Litter-Robot control entity."""
super().__init__(robot=robot, entity_type=entity_type, hub=hub) super().__init__(robot=robot, entity_type=entity_type, hub=hub)
self._refresh_callback: CALLBACK_TYPE | None = None self._refresh_callback: CALLBACK_TYPE | None = None
@ -113,7 +118,7 @@ class LitterRobotControlEntity(LitterRobotEntity):
if time_str is None: if time_str is None:
return 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 None
return ( return (
@ -132,7 +137,9 @@ class LitterRobotConfigEntity(LitterRobotControlEntity):
_attr_entity_category = EntityCategory.CONFIG _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.""" """Init a Litter-Robot control entity."""
super().__init__(robot=robot, entity_type=entity_type, hub=hub) super().__init__(robot=robot, entity_type=entity_type, hub=hub)
self._assumed_state: bool | None = None self._assumed_state: bool | None = None

View file

@ -1,16 +1,17 @@
"""A wrapper 'hub' for the Litter-Robot API.""" """A wrapper 'hub' for the Litter-Robot API."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Generator, Mapping
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any from typing import Any
from pylitterbot import Account from pylitterbot import Account, LitterRobot
from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN from .const import DOMAIN
@ -23,11 +24,10 @@ UPDATE_INTERVAL_SECONDS = 20
class LitterRobotHub: class LitterRobotHub:
"""A Litter-Robot hub wrapper class.""" """A Litter-Robot hub wrapper class."""
account: Account
def __init__(self, hass: HomeAssistant, data: Mapping[str, Any]) -> None: def __init__(self, hass: HomeAssistant, data: Mapping[str, Any]) -> None:
"""Initialize the Litter-Robot hub.""" """Initialize the Litter-Robot hub."""
self._data = data self._data = data
self.account = Account(websession=async_get_clientsession(hass))
async def _async_update_data() -> bool: async def _async_update_data() -> bool:
"""Update all device states from the Litter-Robot API.""" """Update all device states from the Litter-Robot API."""
@ -44,7 +44,6 @@ class LitterRobotHub:
async def login(self, load_robots: bool = False) -> None: async def login(self, load_robots: bool = False) -> None:
"""Login to Litter-Robot.""" """Login to Litter-Robot."""
self.account = Account()
try: try:
await self.account.connect( await self.account.connect(
username=self._data[CONF_USERNAME], username=self._data[CONF_USERNAME],
@ -58,3 +57,9 @@ class LitterRobotHub:
except LitterRobotException as ex: except LitterRobotException as ex:
_LOGGER.error("Unable to connect to Litter-Robot API") _LOGGER.error("Unable to connect to Litter-Robot API")
raise ex 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)
)

View file

@ -3,7 +3,7 @@
"name": "Litter-Robot", "name": "Litter-Robot",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/litterrobot", "documentation": "https://www.home-assistant.io/integrations/litterrobot",
"requirements": ["pylitterbot==2022.7.0"], "requirements": ["pylitterbot==2022.8.0"],
"codeowners": ["@natekspencer"], "codeowners": ["@natekspencer"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pylitterbot"] "loggers": ["pylitterbot"]

View file

@ -1,8 +1,6 @@
"""Support for Litter-Robot selects.""" """Support for Litter-Robot selects."""
from __future__ import annotations from __future__ import annotations
from pylitterbot.robot import VALID_WAIT_TIMES
from homeassistant.components.select import SelectEntity from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -24,12 +22,10 @@ async def async_setup_entry(
hub: LitterRobotHub = hass.data[DOMAIN][config_entry.entry_id] hub: LitterRobotHub = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities( async_add_entities(
[ LitterRobotSelect(
LitterRobotSelect( robot=robot, entity_type=TYPE_CLEAN_CYCLE_WAIT_TIME_MINUTES, hub=hub
robot=robot, entity_type=TYPE_CLEAN_CYCLE_WAIT_TIME_MINUTES, hub=hub )
) for robot in hub.litter_robots()
for robot in hub.account.robots
]
) )
@ -46,7 +42,7 @@ class LitterRobotSelect(LitterRobotConfigEntity, SelectEntity):
@property @property
def options(self) -> list[str]: def options(self) -> list[str]:
"""Return a set of selectable options.""" """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: async def async_select_option(self, option: str) -> None:
"""Change the selected option.""" """Change the selected option."""

View file

@ -6,7 +6,7 @@ from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from typing import Any, Union, cast from typing import Any, Union, cast
from pylitterbot.robot import Robot from pylitterbot import LitterRobot
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -40,7 +40,7 @@ class LitterRobotSensorEntityDescription(SensorEntityDescription):
"""A class that describes Litter-Robot sensor entities.""" """A class that describes Litter-Robot sensor entities."""
icon_fn: Callable[[Any], str | None] = lambda _: None 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): class LitterRobotSensorEntity(LitterRobotEntity, SensorEntity):
@ -50,7 +50,7 @@ class LitterRobotSensorEntity(LitterRobotEntity, SensorEntity):
def __init__( def __init__(
self, self,
robot: Robot, robot: LitterRobot,
hub: LitterRobotHub, hub: LitterRobotHub,
description: LitterRobotSensorEntityDescription, description: LitterRobotSensorEntityDescription,
) -> None: ) -> None:
@ -87,13 +87,13 @@ ROBOT_SENSORS = [
name="Sleep Mode Start Time", name="Sleep Mode Start Time",
key="sleep_mode_start_time", key="sleep_mode_start_time",
device_class=SensorDeviceClass.TIMESTAMP, 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( LitterRobotSensorEntityDescription(
name="Sleep Mode End Time", name="Sleep Mode End Time",
key="sleep_mode_end_time", key="sleep_mode_end_time",
device_class=SensorDeviceClass.TIMESTAMP, 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( LitterRobotSensorEntityDescription(
name="Last Seen", name="Last Seen",
@ -120,5 +120,5 @@ async def async_setup_entry(
async_add_entities( async_add_entities(
LitterRobotSensorEntity(robot=robot, hub=hub, description=description) LitterRobotSensorEntity(robot=robot, hub=hub, description=description)
for description in ROBOT_SENSORS for description in ROBOT_SENSORS
for robot in hub.account.robots for robot in hub.litter_robots()
) )

View file

@ -21,7 +21,7 @@ class LitterRobotNightLightModeSwitch(LitterRobotConfigEntity, SwitchEntity):
"""Return true if switch is on.""" """Return true if switch is on."""
if self._refresh_callback is not None: if self._refresh_callback is not None:
return self._assumed_state return self._assumed_state
return self.robot.night_light_mode_enabled # type: ignore[no-any-return] return self.robot.night_light_mode_enabled
@property @property
def icon(self) -> str: def icon(self) -> str:
@ -45,7 +45,7 @@ class LitterRobotPanelLockoutSwitch(LitterRobotConfigEntity, SwitchEntity):
"""Return true if switch is on.""" """Return true if switch is on."""
if self._refresh_callback is not None: if self._refresh_callback is not None:
return self._assumed_state return self._assumed_state
return self.robot.panel_lock_enabled # type: ignore[no-any-return] return self.robot.panel_lock_enabled
@property @property
def icon(self) -> str: def icon(self) -> str:
@ -76,10 +76,8 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up Litter-Robot switches using config entry.""" """Set up Litter-Robot switches using config entry."""
hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
entities: list[SwitchEntity] = [] switch_class(robot=robot, entity_type=switch_type, hub=hub)
for robot in hub.account.robots: for switch_class, switch_type in ROBOT_SWITCHES
for switch_class, switch_type in ROBOT_SWITCHES: for robot in hub.litter_robots()
entities.append(switch_class(robot=robot, entity_type=switch_type, hub=hub)) )
async_add_entities(entities)

View file

@ -5,7 +5,7 @@ import logging
from typing import Any from typing import Any
from pylitterbot.enums import LitterBoxStatus from pylitterbot.enums import LitterBoxStatus
from pylitterbot.robot import VALID_WAIT_TIMES from pylitterbot.robot.litterrobot import VALID_WAIT_TIMES
import voluptuous as vol import voluptuous as vol
from homeassistant.components.vacuum import ( from homeassistant.components.vacuum import (
@ -56,10 +56,8 @@ async def async_setup_entry(
hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id]
async_add_entities( async_add_entities(
[ LitterRobotCleaner(robot=robot, entity_type=TYPE_LITTER_BOX, hub=hub)
LitterRobotCleaner(robot=robot, entity_type=TYPE_LITTER_BOX, hub=hub) for robot in hub.litter_robots()
for robot in hub.account.robots
]
) )
platform = entity_platform.async_get_current_platform() platform = entity_platform.async_get_current_platform()

View file

@ -1647,7 +1647,7 @@ pylibrespot-java==0.1.0
pylitejet==0.3.0 pylitejet==0.3.0
# homeassistant.components.litterrobot # homeassistant.components.litterrobot
pylitterbot==2022.7.0 pylitterbot==2022.8.0
# homeassistant.components.lutron_caseta # homeassistant.components.lutron_caseta
pylutron-caseta==0.13.1 pylutron-caseta==0.13.1

View file

@ -1148,7 +1148,7 @@ pylibrespot-java==0.1.0
pylitejet==0.3.0 pylitejet==0.3.0
# homeassistant.components.litterrobot # homeassistant.components.litterrobot
pylitterbot==2022.7.0 pylitterbot==2022.8.0
# homeassistant.components.lutron_caseta # homeassistant.components.lutron_caseta
pylutron-caseta==0.13.1 pylutron-caseta==0.13.1

View file

@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Any from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from pylitterbot import Account, Robot from pylitterbot import Account, LitterRobot3, Robot
from pylitterbot.exceptions import InvalidCommandException from pylitterbot.exceptions import InvalidCommandException
import pytest import pytest
@ -23,7 +23,7 @@ def create_mock_robot(
if not robot_data: if not robot_data:
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.start_cleaning = AsyncMock(side_effect=side_effect)
robot.set_power_status = AsyncMock(side_effect=side_effect) robot.set_power_status = AsyncMock(side_effect=side_effect)
robot.reset_waste_drawer = 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_night_light = AsyncMock(side_effect=side_effect)
robot.set_panel_lockout = AsyncMock(side_effect=side_effect) robot.set_panel_lockout = AsyncMock(side_effect=side_effect)
robot.set_wait_time = AsyncMock(side_effect=side_effect) robot.set_wait_time = AsyncMock(side_effect=side_effect)
robot.refresh = AsyncMock(side_effect=side_effect)
return robot return robot

View file

@ -1,7 +1,7 @@
"""Test the Litter-Robot select entity.""" """Test the Litter-Robot select entity."""
from datetime import timedelta from datetime import timedelta
from pylitterbot.robot import VALID_WAIT_TIMES from pylitterbot import LitterRobot3
import pytest import pytest
from homeassistant.components.litterrobot.entity import REFRESH_WAIT_TIME_SECONDS 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} data = {ATTR_ENTITY_ID: SELECT_ENTITY_ID}
count = 0 count = 0
for wait_time in VALID_WAIT_TIMES: for wait_time in LitterRobot3.VALID_WAIT_TIMES:
count += 1 count += 1
data[ATTR_OPTION] = wait_time data[ATTR_OPTION] = wait_time