Add support for Litter-Robot 4 (#75790)
parent
462ec4ced3
commit
b563bd0ae5
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue