Convert LitterRobotHub to a DataUpdateCoordinator (#136283)

This commit is contained in:
Nathan Spencer 2025-01-22 14:20:13 -07:00 committed by GitHub
parent e3c836aa7d
commit 33f966a12e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 119 additions and 119 deletions

View File

@ -4,15 +4,12 @@ from __future__ import annotations
from pylitterbot import FeederRobot, LitterRobot, LitterRobot3, LitterRobot4, Robot
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntry
from .const import DOMAIN
from .hub import LitterRobotHub
type LitterRobotConfigEntry = ConfigEntry[LitterRobotHub]
from .coordinator import LitterRobotConfigEntry, LitterRobotDataUpdateCoordinator
PLATFORMS_BY_TYPE = {
Robot: (
@ -41,11 +38,11 @@ def get_platforms_for_robots(robots: list[Robot]) -> set[Platform]:
async def async_setup_entry(hass: HomeAssistant, entry: LitterRobotConfigEntry) -> bool:
"""Set up Litter-Robot from a config entry."""
hub = LitterRobotHub(hass, entry.data)
await hub.login(load_robots=True, subscribe_for_updates=True)
entry.runtime_data = hub
coordinator = LitterRobotDataUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
if platforms := get_platforms_for_robots(hub.account.robots):
if platforms := get_platforms_for_robots(coordinator.account.robots):
await hass.config_entries.async_forward_entry_setups(entry, platforms)
return True

View File

@ -17,7 +17,7 @@ from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import LitterRobotConfigEntry
from .coordinator import LitterRobotConfigEntry
from .entity import LitterRobotEntity, _RobotT
@ -66,10 +66,12 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Litter-Robot binary sensors using config entry."""
hub = entry.runtime_data
coordinator = entry.runtime_data
async_add_entities(
LitterRobotBinarySensorEntity(robot=robot, hub=hub, description=description)
for robot in hub.account.robots
LitterRobotBinarySensorEntity(
robot=robot, coordinator=coordinator, description=description
)
for robot in coordinator.account.robots
for robot_type, entity_descriptions in BINARY_SENSOR_MAP.items()
if isinstance(robot, robot_type)
for description in entity_descriptions

View File

@ -13,7 +13,7 @@ from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import LitterRobotConfigEntry
from .coordinator import LitterRobotConfigEntry
from .entity import LitterRobotEntity, _RobotT
@ -51,14 +51,15 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Litter-Robot cleaner using config entry."""
hub = entry.runtime_data
entities = [
LitterRobotButtonEntity(robot=robot, hub=hub, description=description)
for robot in hub.account.robots
coordinator = entry.runtime_data
async_add_entities(
LitterRobotButtonEntity(
robot=robot, coordinator=coordinator, description=description
)
for robot in coordinator.account.robots
for robot_type, description in ROBOT_BUTTON_MAP.items()
if isinstance(robot, robot_type)
]
async_add_entities(entities)
)
class LitterRobotButtonEntity(LitterRobotEntity[_RobotT], ButtonEntity):
@ -69,4 +70,4 @@ class LitterRobotButtonEntity(LitterRobotEntity[_RobotT], ButtonEntity):
async def async_press(self) -> None:
"""Press the button."""
await self.entity_description.press_fn(self.robot)
self.coordinator.async_set_updated_data(True)
self.coordinator.async_set_updated_data(None)

View File

@ -1,64 +1,66 @@
"""A wrapper 'hub' for the Litter-Robot API."""
"""The Litter-Robot coordinator."""
from __future__ import annotations
from collections.abc import Generator, Mapping
from collections.abc import Generator
from datetime import timedelta
import logging
from typing import Any
from pylitterbot import Account, FeederRobot, LitterRobot
from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL_SECONDS = 60 * 5
UPDATE_INTERVAL = timedelta(minutes=5)
type LitterRobotConfigEntry = ConfigEntry[LitterRobotDataUpdateCoordinator]
class LitterRobotHub:
"""A Litter-Robot hub wrapper class."""
class LitterRobotDataUpdateCoordinator(DataUpdateCoordinator[None]):
"""The Litter-Robot data update coordinator."""
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))
config_entry: LitterRobotConfigEntry
async def _async_update_data() -> bool:
"""Update all device states from the Litter-Robot API."""
await self.account.refresh_robots()
return True
self.coordinator = DataUpdateCoordinator(
def __init__(
self, hass: HomeAssistant, config_entry: LitterRobotConfigEntry
) -> None:
"""Initialize the Litter-Robot data update coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_method=_async_update_data,
update_interval=timedelta(seconds=UPDATE_INTERVAL_SECONDS),
update_interval=UPDATE_INTERVAL,
)
async def login(
self, load_robots: bool = False, subscribe_for_updates: bool = False
) -> None:
"""Login to Litter-Robot."""
self.account = Account(websession=async_get_clientsession(hass))
async def _async_update_data(self) -> None:
"""Update all device states from the Litter-Robot API."""
await self.account.refresh_robots()
async def _async_setup(self) -> None:
"""Set up the coordinator."""
try:
await self.account.connect(
username=self._data[CONF_USERNAME],
password=self._data[CONF_PASSWORD],
load_robots=load_robots,
subscribe_for_updates=subscribe_for_updates,
username=self.config_entry.data[CONF_USERNAME],
password=self.config_entry.data[CONF_PASSWORD],
load_robots=True,
subscribe_for_updates=True,
)
except LitterRobotLoginException as ex:
raise ConfigEntryAuthFailed("Invalid credentials") from ex
except LitterRobotException as ex:
raise ConfigEntryNotReady("Unable to connect to Litter-Robot API") from ex
raise UpdateFailed("Unable to connect to Litter-Robot API") from ex
def litter_robots(self) -> Generator[LitterRobot]:
"""Get Litter-Robots from the account."""

View File

@ -9,44 +9,39 @@ from pylitterbot.robot import EVENT_UPDATE
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .hub import LitterRobotHub
from .coordinator import LitterRobotDataUpdateCoordinator
_RobotT = TypeVar("_RobotT", bound=Robot)
class LitterRobotEntity(
CoordinatorEntity[DataUpdateCoordinator[bool]], Generic[_RobotT]
CoordinatorEntity[LitterRobotDataUpdateCoordinator], Generic[_RobotT]
):
"""Generic Litter-Robot entity representing common data and methods."""
_attr_has_entity_name = True
def __init__(
self, robot: _RobotT, hub: LitterRobotHub, description: EntityDescription
self,
robot: _RobotT,
coordinator: LitterRobotDataUpdateCoordinator,
description: EntityDescription,
) -> None:
"""Pass coordinator to CoordinatorEntity."""
super().__init__(hub.coordinator)
super().__init__(coordinator)
self.robot = robot
self.hub = hub
self.entity_description = description
self._attr_unique_id = f"{self.robot.serial}-{description.key}"
@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",
model=self.robot.model,
name=self.robot.name,
sw_version=getattr(self.robot, "firmware", None),
self._attr_unique_id = f"{robot.serial}-{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, robot.serial)},
manufacturer="Whisker",
model=robot.model,
name=robot.name,
serial_number=robot.serial,
sw_version=getattr(robot, "firmware", None),
)
async def async_added_to_hass(self) -> None:

View File

@ -14,9 +14,8 @@ from homeassistant.const import EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import LitterRobotConfigEntry
from .coordinator import LitterRobotConfigEntry, LitterRobotDataUpdateCoordinator
from .entity import LitterRobotEntity, _RobotT
from .hub import LitterRobotHub
_CastTypeT = TypeVar("_CastTypeT", int, float, str)
@ -72,14 +71,15 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Litter-Robot selects using config entry."""
hub = entry.runtime_data
entities = [
LitterRobotSelectEntity(robot=robot, hub=hub, description=description)
for robot in hub.account.robots
coordinator = entry.runtime_data
async_add_entities(
LitterRobotSelectEntity(
robot=robot, coordinator=coordinator, description=description
)
for robot in coordinator.account.robots
for robot_type, description in ROBOT_SELECT_MAP.items()
if isinstance(robot, robot_type)
]
async_add_entities(entities)
)
class LitterRobotSelectEntity(
@ -92,11 +92,11 @@ class LitterRobotSelectEntity(
def __init__(
self,
robot: _RobotT,
hub: LitterRobotHub,
coordinator: LitterRobotDataUpdateCoordinator,
description: RobotSelectEntityDescription[_RobotT, _CastTypeT],
) -> None:
"""Initialize a Litter-Robot select entity."""
super().__init__(robot, hub, description)
super().__init__(robot, coordinator, description)
options = self.entity_description.options_fn(self.robot)
self._attr_options = list(map(str, options))

View File

@ -19,7 +19,7 @@ from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfMass
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import LitterRobotConfigEntry
from .coordinator import LitterRobotConfigEntry
from .entity import LitterRobotEntity, _RobotT
@ -159,12 +159,13 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Litter-Robot sensors using config entry."""
hub = entry.runtime_data
entities = [
LitterRobotSensorEntity(robot=robot, hub=hub, description=description)
for robot in hub.account.robots
coordinator = entry.runtime_data
async_add_entities(
LitterRobotSensorEntity(
robot=robot, coordinator=coordinator, description=description
)
for robot in coordinator.account.robots
for robot_type, entity_descriptions in ROBOT_SENSOR_MAP.items()
if isinstance(robot, robot_type)
for description in entity_descriptions
]
async_add_entities(entities)
)

View File

@ -13,7 +13,7 @@ from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import LitterRobotConfigEntry
from .coordinator import LitterRobotConfigEntry
from .entity import LitterRobotEntity, _RobotT
@ -48,14 +48,13 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Litter-Robot switches using config entry."""
hub = entry.runtime_data
entities = [
RobotSwitchEntity(robot=robot, hub=hub, description=description)
coordinator = entry.runtime_data
async_add_entities(
RobotSwitchEntity(robot=robot, coordinator=coordinator, description=description)
for description in ROBOT_SWITCHES
for robot in hub.account.robots
for robot in coordinator.account.robots
if isinstance(robot, (LitterRobot, FeederRobot))
]
async_add_entities(entities)
)
class RobotSwitchEntity(LitterRobotEntity[_RobotT], SwitchEntity):

View File

@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.util.dt as dt_util
from . import LitterRobotConfigEntry
from .coordinator import LitterRobotConfigEntry
from .entity import LitterRobotEntity, _RobotT
@ -52,15 +52,15 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Litter-Robot cleaner using config entry."""
hub = entry.runtime_data
coordinator = entry.runtime_data
async_add_entities(
[
LitterRobotTimeEntity(
robot=robot, hub=hub, description=LITTER_ROBOT_3_SLEEP_START
)
for robot in hub.litter_robots()
if isinstance(robot, LitterRobot3)
]
LitterRobotTimeEntity(
robot=robot,
coordinator=coordinator,
description=LITTER_ROBOT_3_SLEEP_START,
)
for robot in coordinator.litter_robots()
if isinstance(robot, LitterRobot3)
)

View File

@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import LitterRobotConfigEntry
from .coordinator import LitterRobotConfigEntry
from .entity import LitterRobotEntity
SCAN_INTERVAL = timedelta(days=1)
@ -34,12 +34,14 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Litter-Robot update platform."""
hub = entry.runtime_data
entities = [
RobotUpdateEntity(robot=robot, hub=hub, description=FIRMWARE_UPDATE_ENTITY)
for robot in hub.litter_robots()
coordinator = entry.runtime_data
entities = (
RobotUpdateEntity(
robot=robot, coordinator=coordinator, description=FIRMWARE_UPDATE_ENTITY
)
for robot in coordinator.litter_robots()
if isinstance(robot, LitterRobot4)
]
)
async_add_entities(entities, True)

View File

@ -20,7 +20,7 @@ from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.util.dt as dt_util
from . import LitterRobotConfigEntry
from .coordinator import LitterRobotConfigEntry
from .entity import LitterRobotEntity
SERVICE_SET_SLEEP_MODE = "set_sleep_mode"
@ -49,12 +49,13 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Litter-Robot cleaner using config entry."""
hub = entry.runtime_data
entities = [
LitterRobotCleaner(robot=robot, hub=hub, description=LITTER_BOX_ENTITY)
for robot in hub.litter_robots()
]
async_add_entities(entities)
coordinator = entry.runtime_data
async_add_entities(
LitterRobotCleaner(
robot=robot, coordinator=coordinator, description=LITTER_BOX_ENTITY
)
for robot in coordinator.litter_robots()
)
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(

View File

@ -117,7 +117,7 @@ def mock_account_with_side_effects() -> MagicMock:
async def setup_integration(
hass: HomeAssistant, mock_account: MagicMock, platform_domain: str | None = None
) -> MockConfigEntry:
"""Load a Litter-Robot platform with the provided hub."""
"""Load a Litter-Robot platform with the provided coordinator."""
entry = MockConfigEntry(
domain=litterrobot.DOMAIN,
data=CONFIG[litterrobot.DOMAIN],
@ -126,7 +126,7 @@ async def setup_integration(
with (
patch(
"homeassistant.components.litterrobot.hub.Account",
"homeassistant.components.litterrobot.coordinator.Account",
return_value=mock_account,
),
patch(

View File

@ -63,7 +63,7 @@ async def test_entry_not_setup(
entry.add_to_hass(hass)
with patch(
"homeassistant.components.litterrobot.hub.Account.connect",
"homeassistant.components.litterrobot.coordinator.Account.connect",
side_effect=side_effect,
):
await hass.config_entries.async_setup(entry.entry_id)