"""Support for Roborock button.""" from collections.abc import Callable from dataclasses import dataclass import logging from typing import Any, override from roborock.devices.traits.v1.consumeable import ConsumableAttribute from roborock.exceptions import RoborockException from roborock.roborock_message import RoborockZeoProtocol from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN from .coordinator import ( RoborockB01Q10UpdateCoordinator, RoborockConfigEntry, RoborockCoordinatorType, RoborockDataUpdateCoordinator, RoborockDataUpdateCoordinatorA01, RoborockWashingMachineUpdateCoordinator, ) from .entity import ( RoborockCoordinatedEntityA01, RoborockCoordinatedEntityB01Q10, RoborockEntity, RoborockEntityV1, ) _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 @dataclass(frozen=True, kw_only=True) class RoborockButtonDescription(ButtonEntityDescription): """Describes a Roborock button entity.""" attribute: ConsumableAttribute is_dock_entity: bool = False is_supported: Callable[[RoborockDataUpdateCoordinator], bool] = lambda _: True CONSUMABLE_BUTTON_DESCRIPTIONS = [ RoborockButtonDescription( key="reset_sensor_consumable", translation_key="reset_sensor_consumable", attribute=ConsumableAttribute.SENSOR_DIRTY_TIME, entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, ), RoborockButtonDescription( key="reset_air_filter_consumable", translation_key="reset_air_filter_consumable", attribute=ConsumableAttribute.FILTER_WORK_TIME, entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, ), RoborockButtonDescription( key="reset_side_brush_consumable", translation_key="reset_side_brush_consumable", attribute=ConsumableAttribute.SIDE_BRUSH_WORK_TIME, entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, ), RoborockButtonDescription( key="reset_main_brush_consumable", translation_key="reset_main_brush_consumable", attribute=ConsumableAttribute.MAIN_BRUSH_WORK_TIME, entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, ), RoborockButtonDescription( key="reset_dock_strainer_consumable", translation_key="reset_dock_strainer_consumable", attribute=ConsumableAttribute.STRAINER_WORK_TIME, entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, is_dock_entity=True, is_supported=lambda coordinator: ( coordinator.properties_api.wash_towel_mode is not None ), ), RoborockButtonDescription( key="reset_dock_cleaning_brush_consumable", translation_key="reset_dock_cleaning_brush_consumable", attribute=ConsumableAttribute.CLEANING_BRUSH_WORK_TIME, entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, is_dock_entity=True, is_supported=lambda coordinator: ( coordinator.properties_api.wash_towel_mode is not None ), ), ] @dataclass(frozen=True, kw_only=True) class RoborockButtonDescriptionA01(ButtonEntityDescription): """Describes a Roborock A01 button entity.""" data_protocol: RoborockZeoProtocol ZEO_BUTTON_DESCRIPTIONS = [ RoborockButtonDescriptionA01( key="start", data_protocol=RoborockZeoProtocol.START, translation_key="start", ), RoborockButtonDescriptionA01( key="pause", data_protocol=RoborockZeoProtocol.PAUSE, translation_key="pause", ), RoborockButtonDescriptionA01( key="shutdown", data_protocol=RoborockZeoProtocol.SHUTDOWN, translation_key="shutdown", ), ] Q10_BUTTON_DESCRIPTIONS = [ ButtonEntityDescription( key="empty_dustbin", translation_key="empty_dustbin", ), ] async def async_setup_entry( hass: HomeAssistant, config_entry: RoborockConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Roborock button platform.""" coordinators = config_entry.runtime_data @callback def async_add_coordinator_entities( coordinator: RoborockCoordinatorType, ) -> None: """Add entities for a specific coordinator.""" entities: list[ButtonEntity] = [] if isinstance(coordinator, RoborockDataUpdateCoordinator): entities.extend( RoborockButtonEntity(coordinator, description) for description in CONSUMABLE_BUTTON_DESCRIPTIONS if description.is_supported(coordinator) ) async def async_add_routine_buttons() -> None: try: routines = await coordinator.get_routines() except HomeAssistantError as err: _LOGGER.error( "Failed to get routines for %s: %s", coordinator.device.name, err, ) return routine_entities = [ RoborockRoutineButtonEntity( coordinator, ButtonEntityDescription( key=str(routine.id), name=routine.name, ), ) for routine in routines ] async_add_entities(routine_entities) config_entry.async_create_background_task( hass, async_add_routine_buttons(), f"roborock_setup_routines_{coordinator.duid}", ) elif isinstance(coordinator, RoborockWashingMachineUpdateCoordinator): entities.extend( RoborockButtonEntityA01(coordinator, description) for description in ZEO_BUTTON_DESCRIPTIONS ) elif isinstance(coordinator, RoborockB01Q10UpdateCoordinator): entities.extend( RoborockQ10EmptyDustbinButtonEntity(coordinator, description) for description in Q10_BUTTON_DESCRIPTIONS ) async_add_entities(entities) for coordinator in coordinators.values(): async_add_coordinator_entities(coordinator) config_entry.async_on_unload( async_dispatcher_connect( hass, f"roborock_coordinator_added_{config_entry.entry_id}", async_add_coordinator_entities, ) ) class RoborockButtonEntity(RoborockEntityV1, ButtonEntity): """A class to define Roborock button entities.""" entity_description: RoborockButtonDescription def __init__( self, coordinator: RoborockDataUpdateCoordinator, entity_description: RoborockButtonDescription, ) -> None: """Create a button entity.""" device_info = ( coordinator.dock_device_info if entity_description.is_dock_entity else coordinator.device_info ) super().__init__( f"{entity_description.key}_{coordinator.duid_slug}", device_info, api=coordinator.properties_api.command, ) self.entity_description = entity_description self._consumable = coordinator.properties_api.consumables @override async def async_press(self) -> None: """Press the button.""" try: await self._consumable.reset_consumable(self.entity_description.attribute) except RoborockException as err: # This error message could be improved since it is fairly low level # and technical. Can add a more user friendly message with the # name of the attribute being reset. raise HomeAssistantError( translation_domain=DOMAIN, translation_key="command_failed", translation_placeholders={ "command": "RESET_CONSUMABLE", }, ) from err class RoborockRoutineButtonEntity(RoborockEntity, ButtonEntity): """A class to define Roborock routines button entities.""" entity_description: ButtonEntityDescription def __init__( self, coordinator: RoborockDataUpdateCoordinator, entity_description: ButtonEntityDescription, ) -> None: """Create a button entity.""" super().__init__( f"{entity_description.key}_{coordinator.duid_slug}", coordinator.device_info, ) self._routine_id = int(entity_description.key) self._coordinator = coordinator self.entity_description = entity_description @override async def async_press(self, **kwargs: Any) -> None: """Press the button.""" await self._coordinator.execute_routines(self._routine_id) class RoborockButtonEntityA01(RoborockCoordinatedEntityA01, ButtonEntity): """A class to define Roborock A01 button entities.""" entity_description: RoborockButtonDescriptionA01 def __init__( self, coordinator: RoborockDataUpdateCoordinatorA01, entity_description: RoborockButtonDescriptionA01, ) -> None: """Create an A01 button entity.""" self.entity_description = entity_description super().__init__( f"{entity_description.key}_{coordinator.duid_slug}", coordinator ) @override async def async_press(self) -> None: """Press the button.""" try: await self.coordinator.api.set_value( # type: ignore[attr-defined] self.entity_description.data_protocol, 1, ) except RoborockException as err: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="button_press_failed", ) from err finally: await self.coordinator.async_request_refresh() class RoborockQ10EmptyDustbinButtonEntity( RoborockCoordinatedEntityB01Q10, ButtonEntity ): """A class to define Q10 empty dustbin button entity.""" entity_description: ButtonEntityDescription coordinator: RoborockB01Q10UpdateCoordinator def __init__( self, coordinator: RoborockB01Q10UpdateCoordinator, entity_description: ButtonEntityDescription, ) -> None: """Create a Q10 empty dustbin button entity.""" self.entity_description = entity_description super().__init__( f"{entity_description.key}_{coordinator.duid_slug}", coordinator, ) @override async def async_press(self, **kwargs: Any) -> None: """Press the button to empty dustbin.""" try: await self.coordinator.api.vacuum.empty_dustbin() except RoborockException as err: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="command_failed", translation_placeholders={ "command": "empty_dustbin", }, ) from err