From eaaa53d8d5565cf491381b6564b0d7c972d9e787 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 15 Nov 2021 12:09:22 -0700 Subject: [PATCH] Add button to litterrobot (#59734) --- .../components/litterrobot/__init__.py | 7 +- .../components/litterrobot/button.py | 43 +++++++++++++ .../components/litterrobot/vacuum.py | 17 +++++ tests/components/litterrobot/test_button.py | 48 ++++++++++++++ tests/components/litterrobot/test_vacuum.py | 64 ++++++++++--------- 5 files changed, 148 insertions(+), 31 deletions(-) create mode 100644 homeassistant/components/litterrobot/button.py create mode 100644 tests/components/litterrobot/test_button.py diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 17bef9a23a8..d972ecc79d9 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -2,6 +2,11 @@ from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.vacuum import DOMAIN as VACUUM_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -9,7 +14,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN from .hub import LitterRobotHub -PLATFORMS = ["select", "sensor", "switch", "vacuum"] +PLATFORMS = [BUTTON_DOMAIN, SELECT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN, VACUUM_DOMAIN] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/litterrobot/button.py b/homeassistant/components/litterrobot/button.py new file mode 100644 index 00000000000..3b8be295731 --- /dev/null +++ b/homeassistant/components/litterrobot/button.py @@ -0,0 +1,43 @@ +"""Support for Litter-Robot button.""" +from __future__ import annotations + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import LitterRobotEntity +from .hub import LitterRobotHub + +TYPE_RESET_WASTE_DRAWER = "Reset Waste Drawer" + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """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 + ] + ) + + +class LitterRobotResetWasteDrawerButton(LitterRobotEntity, ButtonEntity): + """Litter-Robot reset waste drawer button.""" + + _attr_icon = "mdi:delete-variant" + _attr_entity_category = ENTITY_CATEGORY_CONFIG + + async def async_press(self) -> None: + """Press the button.""" + await self.robot.reset_waste_drawer() + self.coordinator.async_set_updated_data(True) diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 81f9e631beb..5af6c5b5ef3 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -1,6 +1,7 @@ """Support for Litter-Robot "Vacuum".""" from __future__ import annotations +import logging from typing import Any from pylitterbot.enums import LitterBoxStatus @@ -29,6 +30,8 @@ from .const import DOMAIN from .entity import LitterRobotControlEntity from .hub import LitterRobotHub +_LOGGER = logging.getLogger(__name__) + SUPPORT_LITTERROBOT = ( SUPPORT_START | SUPPORT_STATE | SUPPORT_STATUS | SUPPORT_TURN_OFF | SUPPORT_TURN_ON ) @@ -121,6 +124,13 @@ class LitterRobotCleaner(LitterRobotControlEntity, StateVacuumEntity): async def async_reset_waste_drawer(self) -> None: """Reset the waste drawer level.""" + # The Litter-Robot reset waste drawer service has been replaced by a + # dedicated button entity and marked as deprecated + _LOGGER.warning( + "The 'litterrobot.reset_waste_drawer' service is deprecated and " + "replaced by a dedicated reset waste drawer button entity; Please " + "use that entity to reset the waste drawer instead" + ) await self.robot.reset_waste_drawer() self.coordinator.async_set_updated_data(True) @@ -136,6 +146,13 @@ class LitterRobotCleaner(LitterRobotControlEntity, StateVacuumEntity): async def async_set_wait_time(self, minutes: int) -> None: """Set the wait time.""" + # The Litter-Robot set wait time service has been replaced by a + # dedicated select entity and marked as deprecated + _LOGGER.warning( + "The 'litterrobot.set_wait_time' service is deprecated and " + "replaced by a dedicated set wait time select entity; Please " + "use that entity to set the wait time instead" + ) await self.perform_action_and_refresh(self.robot.set_wait_time, minutes) @property diff --git a/tests/components/litterrobot/test_button.py b/tests/components/litterrobot/test_button.py new file mode 100644 index 00000000000..0ca74da5d02 --- /dev/null +++ b/tests/components/litterrobot/test_button.py @@ -0,0 +1,48 @@ +"""Test the Litter-Robot button entity.""" +from unittest.mock import MagicMock + +from freezegun import freeze_time + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_ICON, + ENTITY_CATEGORY_CONFIG, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import setup_integration + +BUTTON_ENTITY = "button.test_reset_waste_drawer" + + +@freeze_time("2021-11-15 17:37:00", tz_offset=-7) +async def test_button(hass: HomeAssistant, mock_account: MagicMock) -> None: + """Test the creation and values of the Litter-Robot button.""" + await setup_integration(hass, mock_account, BUTTON_DOMAIN) + entity_registry = er.async_get(hass) + + state = hass.states.get(BUTTON_ENTITY) + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:delete-variant" + assert state.state == STATE_UNKNOWN + + entry = entity_registry.async_get(BUTTON_ENTITY) + assert entry + assert entry.entity_category == ENTITY_CATEGORY_CONFIG + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: BUTTON_ENTITY}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_account.robots[0].reset_waste_drawer.call_count == 1 + mock_account.robots[0].reset_waste_drawer.assert_called_with() + + state = hass.states.get(BUTTON_ENTITY) + assert state + assert state.state == "2021-11-15T10:37:00+00:00" diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index aa0d38583e2..89f8f077b55 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -1,5 +1,9 @@ """Test the Litter-Robot vacuum entity.""" +from __future__ import annotations + from datetime import timedelta +from typing import Any +from unittest.mock import MagicMock import pytest from voluptuous.error import MultipleInvalid @@ -36,7 +40,7 @@ COMPONENT_SERVICE_DOMAIN = { } -async def test_vacuum(hass: HomeAssistant, mock_account): +async def test_vacuum(hass: HomeAssistant, mock_account: MagicMock) -> None: """Tests the vacuum entity was set up.""" await setup_integration(hass, mock_account, PLATFORM_DOMAIN) assert hass.services.has_service(DOMAIN, SERVICE_RESET_WASTE_DRAWER) @@ -48,8 +52,8 @@ async def test_vacuum(hass: HomeAssistant, mock_account): async def test_vacuum_status_when_sleeping( - hass: HomeAssistant, mock_account_with_sleeping_robot -): + hass: HomeAssistant, mock_account_with_sleeping_robot: MagicMock +) -> None: """Tests the vacuum status when sleeping.""" await setup_integration(hass, mock_account_with_sleeping_robot, PLATFORM_DOMAIN) @@ -58,14 +62,18 @@ async def test_vacuum_status_when_sleeping( assert vacuum.attributes.get(ATTR_STATUS) == "Ready (Sleeping)" -async def test_no_robots(hass: HomeAssistant, mock_account_with_no_robots): +async def test_no_robots( + hass: HomeAssistant, mock_account_with_no_robots: MagicMock +) -> None: """Tests the vacuum entity was set up.""" await setup_integration(hass, mock_account_with_no_robots, PLATFORM_DOMAIN) assert not hass.services.has_service(DOMAIN, SERVICE_RESET_WASTE_DRAWER) -async def test_vacuum_with_error(hass: HomeAssistant, mock_account_with_error): +async def test_vacuum_with_error( + hass: HomeAssistant, mock_account_with_error: MagicMock +) -> None: """Tests a vacuum entity with an error.""" await setup_integration(hass, mock_account_with_error, PLATFORM_DOMAIN) @@ -80,39 +88,34 @@ async def test_vacuum_with_error(hass: HomeAssistant, mock_account_with_error): (SERVICE_START, "start_cleaning", None), (SERVICE_TURN_OFF, "set_power_status", None), (SERVICE_TURN_ON, "set_power_status", None), - ( - SERVICE_RESET_WASTE_DRAWER, - "reset_waste_drawer", - None, - ), + (SERVICE_RESET_WASTE_DRAWER, "reset_waste_drawer", {"deprecated": True}), ( SERVICE_SET_SLEEP_MODE, "set_sleep_mode", - {"enabled": True, "start_time": "22:30"}, + {"data": {"enabled": True, "start_time": "22:30"}}, ), + (SERVICE_SET_SLEEP_MODE, "set_sleep_mode", {"data": {"enabled": True}}), + (SERVICE_SET_SLEEP_MODE, "set_sleep_mode", {"data": {"enabled": False}}), ( - SERVICE_SET_SLEEP_MODE, - "set_sleep_mode", - {"enabled": True}, - ), - ( - SERVICE_SET_SLEEP_MODE, - "set_sleep_mode", - {"enabled": False}, + SERVICE_SET_WAIT_TIME, + "set_wait_time", + {"data": {"minutes": 3}, "deprecated": True}, ), ( SERVICE_SET_WAIT_TIME, "set_wait_time", - {"minutes": 3}, - ), - ( - SERVICE_SET_WAIT_TIME, - "set_wait_time", - {"minutes": "15"}, + {"data": {"minutes": "15"}, "deprecated": True}, ), ], ) -async def test_commands(hass: HomeAssistant, mock_account, service, command, extra): +async def test_commands( + hass: HomeAssistant, + mock_account: MagicMock, + caplog: pytest.LogCaptureFixture, + service: str, + command: str, + extra: dict[str, Any], +) -> None: """Test sending commands to the vacuum.""" await setup_integration(hass, mock_account, PLATFORM_DOMAIN) @@ -120,9 +123,9 @@ async def test_commands(hass: HomeAssistant, mock_account, service, command, ext assert vacuum assert vacuum.state == STATE_DOCKED - data = {ATTR_ENTITY_ID: VACUUM_ENTITY_ID} - if extra: - data.update(extra) + extra = extra or {} + data = {ATTR_ENTITY_ID: VACUUM_ENTITY_ID, **extra.get("data", {})} + deprecated = extra.get("deprecated", False) await hass.services.async_call( COMPONENT_SERVICE_DOMAIN.get(service, PLATFORM_DOMAIN), @@ -133,9 +136,10 @@ async def test_commands(hass: HomeAssistant, mock_account, service, command, ext future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME_SECONDS) async_fire_time_changed(hass, future) getattr(mock_account.robots[0], command).assert_called_once() + assert (f"'{DOMAIN}.{service}' service is deprecated" in caplog.text) is deprecated -async def test_invalid_wait_time(hass: HomeAssistant, mock_account): +async def test_invalid_wait_time(hass: HomeAssistant, mock_account: MagicMock) -> None: """Test an attempt to send an invalid wait time to the vacuum.""" await setup_integration(hass, mock_account, PLATFORM_DOMAIN)