Add support for Litter-Robot 4 (#75790)

pull/77320/head
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:
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)

View File

@ -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)
)

View File

@ -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

View File

@ -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)
)

View File

@ -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"]

View File

@ -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."""

View File

@ -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()
)

View File

@ -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()
)

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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