From 47d6d6c344b21d2afa547291c413405fbf87410e Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 1 Nov 2023 16:34:04 -0400 Subject: [PATCH] Add button platform to Roborock (#103010) * add button platform to roborock * Update tests/components/roborock/test_button.py Co-authored-by: Duco Sebel <74970928+DCSBL@users.noreply.github.com> * Remove device class * improve tests * sort platforms --------- Co-authored-by: Duco Sebel <74970928+DCSBL@users.noreply.github.com> --- homeassistant/components/roborock/button.py | 112 ++++++++++++++++++ homeassistant/components/roborock/const.py | 7 +- .../components/roborock/strings.json | 14 +++ tests/components/roborock/test_button.py | 42 +++++++ 4 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/roborock/button.py create mode 100644 tests/components/roborock/test_button.py diff --git a/homeassistant/components/roborock/button.py b/homeassistant/components/roborock/button.py new file mode 100644 index 00000000000..aba86ccb6b6 --- /dev/null +++ b/homeassistant/components/roborock/button.py @@ -0,0 +1,112 @@ +"""Support for Roborock button.""" +from __future__ import annotations + +from dataclasses import dataclass + +from roborock.roborock_typing import RoborockCommand + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify + +from .const import DOMAIN +from .coordinator import RoborockDataUpdateCoordinator +from .device import RoborockEntity + + +@dataclass +class RoborockButtonDescriptionMixin: + """Define an entity description mixin for button entities.""" + + command: RoborockCommand + param: list | dict | None + + +@dataclass +class RoborockButtonDescription( + ButtonEntityDescription, RoborockButtonDescriptionMixin +): + """Describes a Roborock button entity.""" + + +CONSUMABLE_BUTTON_DESCRIPTIONS = [ + RoborockButtonDescription( + key="reset_sensor_consumable", + icon="mdi:eye-outline", + translation_key="reset_sensor_consumable", + command=RoborockCommand.RESET_CONSUMABLE, + param=["sensor_dirty_time"], + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + ), + RoborockButtonDescription( + key="reset_air_filter_consumable", + icon="mdi:air-filter", + translation_key="reset_air_filter_consumable", + command=RoborockCommand.RESET_CONSUMABLE, + param=["filter_work_time"], + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + ), + RoborockButtonDescription( + key="reset_side_brush_consumable", + icon="mdi:brush", + translation_key="reset_side_brush_consumable", + command=RoborockCommand.RESET_CONSUMABLE, + param=["side_brush_work_time"], + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + ), + RoborockButtonDescription( + key="reset_main_brush_consumable", + icon="mdi:brush", + translation_key="reset_main_brush_consumable", + command=RoborockCommand.RESET_CONSUMABLE, + param=["main_brush_work_time"], + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Roborock button platform.""" + coordinators: dict[str, RoborockDataUpdateCoordinator] = hass.data[DOMAIN][ + config_entry.entry_id + ] + async_add_entities( + RoborockButtonEntity( + f"{description.key}_{slugify(device_id)}", + coordinator, + description, + ) + for device_id, coordinator in coordinators.items() + for description in CONSUMABLE_BUTTON_DESCRIPTIONS + ) + + +class RoborockButtonEntity(RoborockEntity, ButtonEntity): + """A class to define Roborock button entities.""" + + entity_description: RoborockButtonDescription + + def __init__( + self, + unique_id: str, + coordinator: RoborockDataUpdateCoordinator, + entity_description: RoborockButtonDescription, + ) -> None: + """Create a button entity.""" + super().__init__(unique_id, coordinator.device_info, coordinator.api) + self.entity_description = entity_description + + async def async_press(self) -> None: + """Press the button.""" + await self.send(self.entity_description.command, self.entity_description.param) diff --git a/homeassistant/components/roborock/const.py b/homeassistant/components/roborock/const.py index 36078e53b3e..d135f323e90 100644 --- a/homeassistant/components/roborock/const.py +++ b/homeassistant/components/roborock/const.py @@ -7,11 +7,12 @@ CONF_BASE_URL = "base_url" CONF_USER_DATA = "user_data" PLATFORMS = [ - Platform.VACUUM, + Platform.BUTTON, + Platform.BINARY_SENSOR, + Platform.NUMBER, Platform.SELECT, Platform.SENSOR, Platform.SWITCH, Platform.TIME, - Platform.NUMBER, - Platform.BINARY_SENSOR, + Platform.VACUUM, ] diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 06cffcc2291..8841741d4a1 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -44,6 +44,20 @@ "name": "Water shortage" } }, + "button": { + "reset_sensor_consumable": { + "name": "Reset sensor consumable" + }, + "reset_air_filter_consumable": { + "name": "Reset air filter consumable" + }, + "reset_side_brush_consumable": { + "name": "Reset side brush consumable" + }, + "reset_main_brush_consumable": { + "name": "Reset main brush consumable" + } + }, "number": { "volume": { "name": "Volume" diff --git a/tests/components/roborock/test_button.py b/tests/components/roborock/test_button.py new file mode 100644 index 00000000000..3948e0c161a --- /dev/null +++ b/tests/components/roborock/test_button.py @@ -0,0 +1,42 @@ +"""Test Roborock Button platform.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.button import SERVICE_PRESS +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("entity_id"), + [ + ("button.roborock_s7_maxv_reset_sensor_consumable"), + ("button.roborock_s7_maxv_reset_air_filter_consumable"), + ("button.roborock_s7_maxv_reset_side_brush_consumable"), + "button.roborock_s7_maxv_reset_main_brush_consumable", + ], +) +@pytest.mark.freeze_time("2023-10-30 08:50:00") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_update_success( + hass: HomeAssistant, + bypass_api_fixture, + setup_entry: MockConfigEntry, + entity_id: str, +) -> None: + """Test pressing the button entities.""" + # Ensure that the entity exist, as these test can pass even if there is no entity. + assert hass.states.get(entity_id).state == "unknown" + with patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" + ) as mock_send_message: + await hass.services.async_call( + "button", + SERVICE_PRESS, + blocking=True, + target={"entity_id": entity_id}, + ) + assert mock_send_message.assert_called_once + assert hass.states.get(entity_id).state == "2023-10-30T08:50:00+00:00"