Add globe light settings for Litter-Robot 4 (#152190)

This commit is contained in:
Nathan Spencer
2025-09-12 14:55:50 -04:00
committed by GitHub
parent 3de701a9ab
commit 124a63d846
5 changed files with 120 additions and 38 deletions

View File

@@ -31,6 +31,21 @@
"cycle_delay": { "cycle_delay": {
"default": "mdi:timer-outline" "default": "mdi:timer-outline"
}, },
"globe_brightness": {
"default": "mdi:lightbulb-question",
"state": {
"low": "mdi:lightbulb-on-30",
"medium": "mdi:lightbulb-on-50",
"high": "mdi:lightbulb-on"
}
},
"globe_light": {
"state": {
"off": "mdi:lightbulb-off",
"on": "mdi:lightbulb-on",
"auto": "mdi:lightbulb-auto"
}
},
"meal_insert_size": { "meal_insert_size": {
"default": "mdi:scale" "default": "mdi:scale"
} }

View File

@@ -7,7 +7,7 @@ from dataclasses import dataclass
from typing import Any, Generic, TypeVar from typing import Any, Generic, TypeVar
from pylitterbot import FeederRobot, LitterRobot, LitterRobot4, Robot from pylitterbot import FeederRobot, LitterRobot, LitterRobot4, Robot
from pylitterbot.robot.litterrobot4 import BrightnessLevel from pylitterbot.robot.litterrobot4 import BrightnessLevel, NightLightMode
from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory, UnitOfTime from homeassistant.const import EntityCategory, UnitOfTime
@@ -32,35 +32,73 @@ class RobotSelectEntityDescription(
select_fn: Callable[[_WhiskerEntityT, str], Coroutine[Any, Any, bool]] select_fn: Callable[[_WhiskerEntityT, str], Coroutine[Any, Any, bool]]
ROBOT_SELECT_MAP: dict[type[Robot], RobotSelectEntityDescription] = { ROBOT_SELECT_MAP: dict[type[Robot], tuple[RobotSelectEntityDescription, ...]] = {
LitterRobot: RobotSelectEntityDescription[LitterRobot, int]( # type: ignore[type-abstract] # only used for isinstance check LitterRobot: (
key="cycle_delay", RobotSelectEntityDescription[LitterRobot, int]( # type: ignore[type-abstract] # only used for isinstance check
translation_key="cycle_delay", key="cycle_delay",
unit_of_measurement=UnitOfTime.MINUTES, translation_key="cycle_delay",
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, opt: robot.set_wait_time(int(opt)), options_fn=lambda robot: robot.VALID_WAIT_TIMES,
), select_fn=lambda robot, opt: robot.set_wait_time(int(opt)),
LitterRobot4: RobotSelectEntityDescription[LitterRobot4, str](
key="panel_brightness",
translation_key="brightness_level",
current_fn=(
lambda robot: bri.name.lower()
if (bri := robot.panel_brightness) is not None
else None
),
options_fn=lambda _: [level.name.lower() for level in BrightnessLevel],
select_fn=(
lambda robot, opt: robot.set_panel_brightness(BrightnessLevel[opt.upper()])
), ),
), ),
FeederRobot: RobotSelectEntityDescription[FeederRobot, float]( LitterRobot4: (
key="meal_insert_size", RobotSelectEntityDescription[LitterRobot4, str](
translation_key="meal_insert_size", key="globe_brightness",
unit_of_measurement="cups", translation_key="globe_brightness",
current_fn=lambda robot: robot.meal_insert_size, current_fn=(
options_fn=lambda robot: robot.VALID_MEAL_INSERT_SIZES, lambda robot: bri.name.lower()
select_fn=lambda robot, opt: robot.set_meal_insert_size(float(opt)), if (bri := robot.night_light_level) is not None
else None
),
options_fn=lambda _: [level.name.lower() for level in BrightnessLevel],
select_fn=(
lambda robot, opt: robot.set_night_light_brightness(
BrightnessLevel[opt.upper()]
)
),
),
RobotSelectEntityDescription[LitterRobot4, str](
key="globe_light",
translation_key="globe_light",
current_fn=(
lambda robot: mode.name.lower()
if (mode := robot.night_light_mode) is not None
else None
),
options_fn=lambda _: [mode.name.lower() for mode in NightLightMode],
select_fn=(
lambda robot, opt: robot.set_night_light_mode(
NightLightMode[opt.upper()]
)
),
),
RobotSelectEntityDescription[LitterRobot4, str](
key="panel_brightness",
translation_key="brightness_level",
current_fn=(
lambda robot: bri.name.lower()
if (bri := robot.panel_brightness) is not None
else None
),
options_fn=lambda _: [level.name.lower() for level in BrightnessLevel],
select_fn=(
lambda robot, opt: robot.set_panel_brightness(
BrightnessLevel[opt.upper()]
)
),
),
),
FeederRobot: (
RobotSelectEntityDescription[FeederRobot, float](
key="meal_insert_size",
translation_key="meal_insert_size",
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)),
),
), ),
} }
@@ -77,8 +115,9 @@ async def async_setup_entry(
robot=robot, coordinator=coordinator, description=description robot=robot, coordinator=coordinator, description=description
) )
for robot in coordinator.account.robots for robot in coordinator.account.robots
for robot_type, description in ROBOT_SELECT_MAP.items() for robot_type, descriptions in ROBOT_SELECT_MAP.items()
if isinstance(robot, robot_type) if isinstance(robot, robot_type)
for description in descriptions
) )

View File

@@ -144,6 +144,22 @@
"cycle_delay": { "cycle_delay": {
"name": "Clean cycle wait time minutes" "name": "Clean cycle wait time minutes"
}, },
"globe_brightness": {
"name": "Globe brightness",
"state": {
"low": "[%key:common::state::low%]",
"medium": "[%key:common::state::medium%]",
"high": "[%key:common::state::high%]"
}
},
"globe_light": {
"name": "Globe light",
"state": {
"auto": "[%key:common::state::auto%]",
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]"
}
},
"meal_insert_size": { "meal_insert_size": {
"name": "Meal insert size" "name": "Meal insert size"
}, },

View File

@@ -39,8 +39,9 @@ ROBOT_4_DATA = {
"cleanCycleWaitTime": 15, "cleanCycleWaitTime": 15,
"isKeypadLockout": False, "isKeypadLockout": False,
"nightLightMode": "OFF", "nightLightMode": "OFF",
"nightLightBrightness": 85, "nightLightBrightness": 50,
"isPanelSleepMode": False, "isPanelSleepMode": False,
"panelBrightnessHigh": 50,
"panelSleepTime": 0, "panelSleepTime": 0,
"panelWakeTime": 0, "panelWakeTime": 0,
"weekdaySleepModeEnabled": { "weekdaySleepModeEnabled": {

View File

@@ -19,7 +19,6 @@ 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(
@@ -69,26 +68,38 @@ async def test_invalid_wait_time_select(hass: HomeAssistant, mock_account) -> No
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( @pytest.mark.parametrize(
("entity_id", "initial_value", "robot_command"),
[
("select.test_globe_brightness", "medium", "set_night_light_brightness"),
("select.test_globe_light", "off", "set_night_light_mode"),
("select.test_panel_brightness", "medium", "set_panel_brightness"),
],
)
async def test_litterrobot_4_select(
hass: HomeAssistant, hass: HomeAssistant,
mock_account_with_litterrobot_4: MagicMock, mock_account_with_litterrobot_4: MagicMock,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
entity_id: str,
initial_value: str,
robot_command: str,
) -> None: ) -> None:
"""Tests the wait time select entity.""" """Tests a Litter-Robot 4 select entity."""
await setup_integration(hass, mock_account_with_litterrobot_4, SELECT_DOMAIN) await setup_integration(hass, mock_account_with_litterrobot_4, SELECT_DOMAIN)
select = hass.states.get(PANEL_BRIGHTNESS_ENTITY_ID) select = hass.states.get(entity_id)
assert select assert select
assert len(select.attributes[ATTR_OPTIONS]) == 3 assert len(select.attributes[ATTR_OPTIONS]) == 3
assert select.state == initial_value
entity_entry = entity_registry.async_get(PANEL_BRIGHTNESS_ENTITY_ID) entity_entry = entity_registry.async_get(entity_id)
assert entity_entry assert entity_entry
assert entity_entry.entity_category is EntityCategory.CONFIG assert entity_entry.entity_category is EntityCategory.CONFIG
data = {ATTR_ENTITY_ID: PANEL_BRIGHTNESS_ENTITY_ID} data = {ATTR_ENTITY_ID: entity_id}
robot: LitterRobot4 = mock_account_with_litterrobot_4.robots[0] robot: LitterRobot4 = mock_account_with_litterrobot_4.robots[0]
robot.set_panel_brightness = AsyncMock(return_value=True) setattr(robot, robot_command, AsyncMock(return_value=True))
for count, option in enumerate(select.attributes[ATTR_OPTIONS]): for count, option in enumerate(select.attributes[ATTR_OPTIONS]):
data[ATTR_OPTION] = option data[ATTR_OPTION] = option
@@ -100,4 +111,4 @@ async def test_panel_brightness_select(
blocking=True, blocking=True,
) )
assert robot.set_panel_brightness.call_count == count + 1 assert getattr(robot, robot_command).call_count == count + 1