Add panel brightness control for Litter-Robot 4 (#86269)

* Add panel brightness control for Litter-Robot 4

* Use translation_key

* Fix test
This commit is contained in:
Nathan Spencer 2023-03-28 07:07:09 -06:00 committed by GitHub
parent 091932c3ac
commit cdefc48fcd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 111 additions and 39 deletions

View file

@ -12,5 +12,5 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pylitterbot"], "loggers": ["pylitterbot"],
"requirements": ["pylitterbot==2023.1.1"] "requirements": ["pylitterbot==2023.1.2"]
} }

View file

@ -3,10 +3,10 @@ from __future__ import annotations
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
from dataclasses import dataclass from dataclasses import dataclass
import itertools
from typing import Any, Generic, TypeVar from typing import Any, Generic, TypeVar
from pylitterbot import FeederRobot, LitterRobot from pylitterbot import FeederRobot, LitterRobot, LitterRobot4, Robot
from pylitterbot.robot.litterrobot4 import BrightnessLevel
from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -18,14 +18,21 @@ from .const import DOMAIN
from .entity import LitterRobotEntity, _RobotT from .entity import LitterRobotEntity, _RobotT
from .hub import LitterRobotHub from .hub import LitterRobotHub
_CastTypeT = TypeVar("_CastTypeT", int, float) _CastTypeT = TypeVar("_CastTypeT", int, float, str)
BRIGHTNESS_LEVEL_ICON_MAP: dict[BrightnessLevel | None, str] = {
BrightnessLevel.LOW: "mdi:lightbulb-on-30",
BrightnessLevel.MEDIUM: "mdi:lightbulb-on-50",
BrightnessLevel.HIGH: "mdi:lightbulb-on",
None: "mdi:lightbulb-question",
}
@dataclass @dataclass
class RequiredKeysMixin(Generic[_RobotT, _CastTypeT]): class RequiredKeysMixin(Generic[_RobotT, _CastTypeT]):
"""A class that describes robot select entity required keys.""" """A class that describes robot select entity required keys."""
current_fn: Callable[[_RobotT], _CastTypeT] current_fn: Callable[[_RobotT], _CastTypeT | None]
options_fn: Callable[[_RobotT], list[_CastTypeT]] options_fn: Callable[[_RobotT], list[_CastTypeT]]
select_fn: Callable[[_RobotT, str], Coroutine[Any, Any, bool]] select_fn: Callable[[_RobotT, str], Coroutine[Any, Any, bool]]
@ -37,26 +44,42 @@ class RobotSelectEntityDescription(
"""A class that describes robot select entities.""" """A class that describes robot select entities."""
entity_category: EntityCategory = EntityCategory.CONFIG entity_category: EntityCategory = EntityCategory.CONFIG
icon_fn: Callable[[_RobotT], str] | None = None
LITTER_ROBOT_SELECT = RobotSelectEntityDescription[LitterRobot, int]( ROBOT_SELECT_MAP: dict[type[Robot], RobotSelectEntityDescription] = {
key="cycle_delay", LitterRobot: RobotSelectEntityDescription[LitterRobot, int](
name="Clean cycle wait time minutes", key="cycle_delay",
icon="mdi:timer-outline", name="Clean cycle wait time minutes",
unit_of_measurement=UnitOfTime.MINUTES, icon="mdi:timer-outline",
current_fn=lambda robot: robot.clean_cycle_wait_time_minutes, unit_of_measurement=UnitOfTime.MINUTES,
options_fn=lambda robot: robot.VALID_WAIT_TIMES, current_fn=lambda robot: robot.clean_cycle_wait_time_minutes,
select_fn=lambda robot, option: robot.set_wait_time(int(option)), options_fn=lambda robot: robot.VALID_WAIT_TIMES,
) select_fn=lambda robot, opt: robot.set_wait_time(int(opt)),
FEEDER_ROBOT_SELECT = RobotSelectEntityDescription[FeederRobot, float]( ),
key="meal_insert_size", LitterRobot4: RobotSelectEntityDescription[LitterRobot4, str](
name="Meal insert size", key="panel_brightness",
icon="mdi:scale", name="Panel brightness",
unit_of_measurement="cups", translation_key="brightness_level",
current_fn=lambda robot: robot.meal_insert_size, current_fn=lambda robot: bri.name.lower()
options_fn=lambda robot: robot.VALID_MEAL_INSERT_SIZES, if (bri := robot.panel_brightness) is not None
select_fn=lambda robot, option: robot.set_meal_insert_size(float(option)), else None,
) options_fn=lambda _: [level.name.lower() for level in BrightnessLevel],
select_fn=lambda robot, opt: robot.set_panel_brightness(
BrightnessLevel[opt.upper()]
),
icon_fn=lambda robot: BRIGHTNESS_LEVEL_ICON_MAP[robot.panel_brightness],
),
FeederRobot: RobotSelectEntityDescription[FeederRobot, float](
key="meal_insert_size",
name="Meal insert size",
icon="mdi:scale",
unit_of_measurement="cups",
current_fn=lambda robot: robot.meal_insert_size,
options_fn=lambda robot: robot.VALID_MEAL_INSERT_SIZES,
select_fn=lambda robot, opt: robot.set_meal_insert_size(float(opt)),
),
}
async def async_setup_entry( async def async_setup_entry(
@ -66,22 +89,16 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up Litter-Robot selects using config entry.""" """Set up Litter-Robot selects using config entry."""
hub: LitterRobotHub = hass.data[DOMAIN][config_entry.entry_id] hub: LitterRobotHub = hass.data[DOMAIN][config_entry.entry_id]
entities: list[LitterRobotSelect] = list( entities = [
itertools.chain( LitterRobotSelectEntity(robot=robot, hub=hub, description=description)
( for robot in hub.account.robots
LitterRobotSelect(robot=robot, hub=hub, description=LITTER_ROBOT_SELECT) for robot_type, description in ROBOT_SELECT_MAP.items()
for robot in hub.litter_robots() if isinstance(robot, robot_type)
), ]
(
LitterRobotSelect(robot=robot, hub=hub, description=FEEDER_ROBOT_SELECT)
for robot in hub.feeder_robots()
),
)
)
async_add_entities(entities) async_add_entities(entities)
class LitterRobotSelect( class LitterRobotSelectEntity(
LitterRobotEntity[_RobotT], SelectEntity, Generic[_RobotT, _CastTypeT] LitterRobotEntity[_RobotT], SelectEntity, Generic[_RobotT, _CastTypeT]
): ):
"""Litter-Robot Select.""" """Litter-Robot Select."""
@ -99,6 +116,13 @@ class LitterRobotSelect(
options = self.entity_description.options_fn(self.robot) options = self.entity_description.options_fn(self.robot)
self._attr_options = list(map(str, options)) self._attr_options = list(map(str, options))
@property
def icon(self) -> str | None:
"""Return the icon to use in the frontend, if any."""
if icon_fn := self.entity_description.icon_fn:
return str(icon_fn(self.robot))
return super().icon
@property @property
def current_option(self) -> str | None: def current_option(self) -> str | None:
"""Return the selected entity option to represent the entity state.""" """Return the selected entity option to represent the entity state."""

View file

@ -62,6 +62,15 @@
"spf": "Pinch Detect At Startup" "spf": "Pinch Detect At Startup"
} }
} }
},
"select": {
"brightness_level": {
"state": {
"low": "Low",
"medium": "Medium",
"high": "High"
}
}
} }
} }
} }

View file

@ -1753,7 +1753,7 @@ pylibrespot-java==0.1.1
pylitejet==0.5.0 pylitejet==0.5.0
# homeassistant.components.litterrobot # homeassistant.components.litterrobot
pylitterbot==2023.1.1 pylitterbot==2023.1.2
# homeassistant.components.lutron_caseta # homeassistant.components.lutron_caseta
pylutron-caseta==0.18.1 pylutron-caseta==0.18.1

View file

@ -1269,7 +1269,7 @@ pylibrespot-java==0.1.1
pylitejet==0.5.0 pylitejet==0.5.0
# homeassistant.components.litterrobot # homeassistant.components.litterrobot
pylitterbot==2023.1.1 pylitterbot==2023.1.2
# homeassistant.components.lutron_caseta # homeassistant.components.lutron_caseta
pylutron-caseta==0.18.1 pylutron-caseta==0.18.1

View file

@ -1,9 +1,12 @@
"""Test the Litter-Robot select entity.""" """Test the Litter-Robot select entity."""
from pylitterbot import LitterRobot3 from unittest.mock import AsyncMock, MagicMock
from pylitterbot import LitterRobot3, LitterRobot4
import pytest import pytest
from homeassistant.components.select import ( from homeassistant.components.select import (
ATTR_OPTION, ATTR_OPTION,
ATTR_OPTIONS,
DOMAIN as PLATFORM_DOMAIN, DOMAIN as PLATFORM_DOMAIN,
SERVICE_SELECT_OPTION, SERVICE_SELECT_OPTION,
) )
@ -14,6 +17,7 @@ from homeassistant.helpers import entity_registry as er
from .conftest import setup_integration from .conftest import setup_integration
SELECT_ENTITY_ID = "select.test_clean_cycle_wait_time_minutes" SELECT_ENTITY_ID = "select.test_clean_cycle_wait_time_minutes"
PANEL_BRIGHTNESS_ENTITY_ID = "select.test_panel_brightness"
async def test_wait_time_select( async def test_wait_time_select(
@ -63,3 +67,38 @@ async def test_invalid_wait_time_select(hass: HomeAssistant, mock_account) -> No
blocking=True, blocking=True,
) )
assert not mock_account.robots[0].set_wait_time.called assert not mock_account.robots[0].set_wait_time.called
async def test_panel_brightness_select(
hass: HomeAssistant,
mock_account_with_litterrobot_4: MagicMock,
entity_registry: er.EntityRegistry,
) -> None:
"""Tests the wait time select entity."""
await setup_integration(hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN)
select = hass.states.get(PANEL_BRIGHTNESS_ENTITY_ID)
assert select
assert len(select.attributes[ATTR_OPTIONS]) == 3
entity_entry = entity_registry.async_get(PANEL_BRIGHTNESS_ENTITY_ID)
assert entity_entry
assert entity_entry.entity_category is EntityCategory.CONFIG
data = {ATTR_ENTITY_ID: PANEL_BRIGHTNESS_ENTITY_ID}
robot: LitterRobot4 = mock_account_with_litterrobot_4.robots[0]
robot.set_panel_brightness = AsyncMock(return_value=True)
count = 0
for option in select.attributes[ATTR_OPTIONS]:
count += 1
data[ATTR_OPTION] = option
await hass.services.async_call(
PLATFORM_DOMAIN,
SERVICE_SELECT_OPTION,
data,
blocking=True,
)
assert robot.set_panel_brightness.call_count == count