diff --git a/homeassistant/components/cookidoo/__init__.py b/homeassistant/components/cookidoo/__init__.py index bb78f2a569d..d1879b8255a 100644 --- a/homeassistant/components/cookidoo/__init__.py +++ b/homeassistant/components/cookidoo/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .coordinator import CookidooConfigEntry, CookidooDataUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.TODO] +PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.TODO] async def async_setup_entry(hass: HomeAssistant, entry: CookidooConfigEntry) -> bool: diff --git a/homeassistant/components/cookidoo/button.py b/homeassistant/components/cookidoo/button.py new file mode 100644 index 00000000000..2a20a156db4 --- /dev/null +++ b/homeassistant/components/cookidoo/button.py @@ -0,0 +1,70 @@ +"""Support for Cookidoo buttons.""" + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from cookidoo_api import Cookidoo, CookidooException + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import CookidooConfigEntry, CookidooDataUpdateCoordinator +from .entity import CookidooBaseEntity + +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class CookidooButtonEntityDescription(ButtonEntityDescription): + """Describes cookidoo button entity.""" + + press_fn: Callable[[Cookidoo], Awaitable[None]] + + +TODO_CLEAR = CookidooButtonEntityDescription( + key="todo_clear", + translation_key="todo_clear", + press_fn=lambda client: client.clear_shopping_list(), + entity_registry_enabled_default=False, +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: CookidooConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Cookidoo button entities based on a config entry.""" + coordinator = entry.runtime_data + + async_add_entities([CookidooButton(coordinator, TODO_CLEAR)]) + + +class CookidooButton(CookidooBaseEntity, ButtonEntity): + """Defines an Cookidoo button.""" + + entity_description: CookidooButtonEntityDescription + + def __init__( + self, + coordinator: CookidooDataUpdateCoordinator, + description: CookidooButtonEntityDescription, + ) -> None: + """Initialize cookidoo button.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" + + async def async_press(self) -> None: + """Press the button.""" + try: + await self.entity_description.press_fn(self.coordinator.cookidoo) + except CookidooException as e: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="button_clear_todo_failed", + ) from e + await self.coordinator.async_refresh() diff --git a/homeassistant/components/cookidoo/icons.json b/homeassistant/components/cookidoo/icons.json index 36c0724331a..0e411a70fc2 100644 --- a/homeassistant/components/cookidoo/icons.json +++ b/homeassistant/components/cookidoo/icons.json @@ -1,5 +1,10 @@ { "entity": { + "button": { + "todo_clear": { + "default": "mdi:cart-off" + } + }, "todo": { "ingredient_list": { "default": "mdi:cart-plus" diff --git a/homeassistant/components/cookidoo/strings.json b/homeassistant/components/cookidoo/strings.json index 14344bed13d..83cc182be16 100644 --- a/homeassistant/components/cookidoo/strings.json +++ b/homeassistant/components/cookidoo/strings.json @@ -48,6 +48,11 @@ } }, "entity": { + "button": { + "todo_clear": { + "name": "Clear shopping list and additional purchases" + } + }, "todo": { "ingredient_list": { "name": "Shopping list" @@ -58,6 +63,9 @@ } }, "exceptions": { + "button_clear_todo_failed": { + "message": "Failed to clear all items from the Cookidoo shopping list" + }, "todo_save_item_failed": { "message": "Failed to save {name} to Cookidoo shopping list" }, diff --git a/tests/components/cookidoo/conftest.py b/tests/components/cookidoo/conftest.py index 68700967d35..66c2064eb3a 100644 --- a/tests/components/cookidoo/conftest.py +++ b/tests/components/cookidoo/conftest.py @@ -58,6 +58,7 @@ def mock_cookidoo_client() -> Generator[AsyncMock]: "data" ] ] + client.login.return_value = None yield client diff --git a/tests/components/cookidoo/snapshots/test_button.ambr b/tests/components/cookidoo/snapshots/test_button.ambr new file mode 100644 index 00000000000..60f9e95bee7 --- /dev/null +++ b/tests/components/cookidoo/snapshots/test_button.ambr @@ -0,0 +1,47 @@ +# serializer version: 1 +# name: test_all_entities[button.cookidoo_clear_shopping_list_and_additional_purchases-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.cookidoo_clear_shopping_list_and_additional_purchases', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Clear shopping list and additional purchases', + 'platform': 'cookidoo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'todo_clear', + 'unique_id': '01JBVVVJ87F6G5V0QJX6HBC94T_todo_clear', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[button.cookidoo_clear_shopping_list_and_additional_purchases-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Cookidoo Clear shopping list and additional purchases', + }), + 'context': , + 'entity_id': 'button.cookidoo_clear_shopping_list_and_additional_purchases', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/cookidoo/test_button.py b/tests/components/cookidoo/test_button.py new file mode 100644 index 00000000000..3e832ec9fe6 --- /dev/null +++ b/tests/components/cookidoo/test_button.py @@ -0,0 +1,84 @@ +"""Tests for the Cookidoo button platform.""" + +from unittest.mock import AsyncMock, patch + +from cookidoo_api import CookidooRequestException +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_cookidoo_client: AsyncMock, + cookidoo_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.cookidoo.PLATFORMS", [Platform.BUTTON]): + await setup_integration(hass, cookidoo_config_entry) + + assert cookidoo_config_entry.state is ConfigEntryState.LOADED + + await snapshot_platform( + hass, entity_registry, snapshot, cookidoo_config_entry.entry_id + ) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_pressing_button( + hass: HomeAssistant, + mock_cookidoo_client: AsyncMock, + cookidoo_config_entry: MockConfigEntry, +) -> None: + """Test pressing button.""" + await setup_integration(hass, cookidoo_config_entry) + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.cookidoo_clear_shopping_list_and_additional_purchases", + }, + blocking=True, + ) + mock_cookidoo_client.clear_shopping_list.assert_called_once() + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_pressing_button_exception( + hass: HomeAssistant, + mock_cookidoo_client: AsyncMock, + cookidoo_config_entry: MockConfigEntry, +) -> None: + """Test pressing button with exception.""" + + await setup_integration(hass, cookidoo_config_entry) + + assert cookidoo_config_entry.state is ConfigEntryState.LOADED + + mock_cookidoo_client.clear_shopping_list.side_effect = CookidooRequestException + with pytest.raises( + HomeAssistantError, + match="Failed to clear all items from the Cookidoo shopping list", + ): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.cookidoo_clear_shopping_list_and_additional_purchases", + }, + blocking=True, + ) diff --git a/tests/components/cookidoo/test_todo.py b/tests/components/cookidoo/test_todo.py index 0e60a86d225..d66c4f357c2 100644 --- a/tests/components/cookidoo/test_todo.py +++ b/tests/components/cookidoo/test_todo.py @@ -50,7 +50,8 @@ async def test_todo( ) -> None: """Snapshot test states of todo platform.""" - await setup_integration(hass, cookidoo_config_entry) + with patch("homeassistant.components.cookidoo.PLATFORMS", [Platform.TODO]): + await setup_integration(hass, cookidoo_config_entry) assert cookidoo_config_entry.state is ConfigEntryState.LOADED