From 0a781b8fa26a97863a0c8519ba407302cfe32ca3 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:24:09 +0200 Subject: [PATCH] Add button platform to Husqvarna Automower (#119856) * Add button platform to Husqvarna Automower * test coverage * adapt to library changes * Address review --- .../husqvarna_automower/__init__.py | 1 + .../components/husqvarna_automower/button.py | 61 ++++++++++ .../husqvarna_automower/strings.json | 10 ++ .../snapshots/test_button.ambr | 47 ++++++++ .../husqvarna_automower/test_button.py | 112 ++++++++++++++++++ 5 files changed, 231 insertions(+) create mode 100644 homeassistant/components/husqvarna_automower/button.py create mode 100644 tests/components/husqvarna_automower/snapshots/test_button.ambr create mode 100644 tests/components/husqvarna_automower/test_button.py diff --git a/homeassistant/components/husqvarna_automower/__init__.py b/homeassistant/components/husqvarna_automower/__init__.py index e62badd7e7c..326a9a010ef 100644 --- a/homeassistant/components/husqvarna_automower/__init__.py +++ b/homeassistant/components/husqvarna_automower/__init__.py @@ -18,6 +18,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.DEVICE_TRACKER, Platform.LAWN_MOWER, Platform.NUMBER, diff --git a/homeassistant/components/husqvarna_automower/button.py b/homeassistant/components/husqvarna_automower/button.py new file mode 100644 index 00000000000..60c05b92a31 --- /dev/null +++ b/homeassistant/components/husqvarna_automower/button.py @@ -0,0 +1,61 @@ +"""Creates a button entity for Husqvarna Automower integration.""" + +import logging + +from aioautomower.exceptions import ApiException + +from homeassistant.components.button import ButtonEntity +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AutomowerConfigEntry +from .const import DOMAIN +from .coordinator import AutomowerDataUpdateCoordinator +from .entity import AutomowerControlEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AutomowerConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up button platform.""" + coordinator = entry.runtime_data + async_add_entities( + AutomowerButtonEntity(mower_id, coordinator) for mower_id in coordinator.data + ) + + +class AutomowerButtonEntity(AutomowerControlEntity, ButtonEntity): + """Defining the AutomowerButtonEntity.""" + + _attr_translation_key = "confirm_error" + _attr_entity_registry_enabled_default = False + + def __init__( + self, + mower_id: str, + coordinator: AutomowerDataUpdateCoordinator, + ) -> None: + """Set up button platform.""" + super().__init__(mower_id, coordinator) + self._attr_unique_id = f"{mower_id}_confirm_error" + + @property + def available(self) -> bool: + """Return True if the device and entity is available.""" + return super().available and self.mower_attributes.mower.is_error_confirmable + + async def async_press(self) -> None: + """Handle the button press.""" + try: + await self.coordinator.api.commands.error_confirm(self.mower_id) + except ApiException as exception: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="command_send_failed", + translation_placeholders={"exception": str(exception)}, + ) from exception diff --git a/homeassistant/components/husqvarna_automower/strings.json b/homeassistant/components/husqvarna_automower/strings.json index c94a8d0f6d1..a403a56cc5e 100644 --- a/homeassistant/components/husqvarna_automower/strings.json +++ b/homeassistant/components/husqvarna_automower/strings.json @@ -42,6 +42,11 @@ "name": "Returning to dock" } }, + "button": { + "confirm_error": { + "name": "Confirm error" + } + }, "number": { "cutting_height": { "name": "Cutting height" @@ -259,5 +264,10 @@ "name": "Avoid {stay_out_zone}" } } + }, + "exceptions": { + "command_send_failed": { + "message": "Failed to send command: {exception}" + } } } diff --git a/tests/components/husqvarna_automower/snapshots/test_button.ambr b/tests/components/husqvarna_automower/snapshots/test_button.ambr new file mode 100644 index 00000000000..ab2cb427f1a --- /dev/null +++ b/tests/components/husqvarna_automower/snapshots/test_button.ambr @@ -0,0 +1,47 @@ +# serializer version: 1 +# name: test_button_snapshot[button.test_mower_1_confirm_error-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.test_mower_1_confirm_error', + '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': 'Confirm error', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'confirm_error', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_confirm_error', + 'unit_of_measurement': None, + }) +# --- +# name: test_button_snapshot[button.test_mower_1_confirm_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Mower 1 Confirm error', + }), + 'context': , + 'entity_id': 'button.test_mower_1_confirm_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- diff --git a/tests/components/husqvarna_automower/test_button.py b/tests/components/husqvarna_automower/test_button.py new file mode 100644 index 00000000000..6cc465df74b --- /dev/null +++ b/tests/components/husqvarna_automower/test_button.py @@ -0,0 +1,112 @@ +"""Tests for button platform.""" + +import datetime +from unittest.mock import AsyncMock, patch + +from aioautomower.exceptions import ApiException +from aioautomower.utils import mower_list_to_dictionary_dataclass +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.button import SERVICE_PRESS +from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration +from .const import TEST_MOWER_ID + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_json_value_fixture, + snapshot_platform, +) + + +@pytest.mark.freeze_time(datetime.datetime(2024, 2, 29, 11, tzinfo=datetime.UTC)) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_button_states_and_commands( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test button commands.""" + entity_id = "button.test_mower_1_confirm_error" + await setup_integration(hass, mock_config_entry) + state = hass.states.get(entity_id) + assert state.name == "Test Mower 1 Confirm error" + assert state.state == STATE_UNAVAILABLE + + values = mower_list_to_dictionary_dataclass( + load_json_value_fixture("mower.json", DOMAIN) + ) + values[TEST_MOWER_ID].mower.is_error_confirmable = None + mock_automower_client.get_status.return_value = values + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + + values[TEST_MOWER_ID].mower.is_error_confirmable = True + mock_automower_client.get_status.return_value = values + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_UNKNOWN + + await hass.services.async_call( + domain="button", + service=SERVICE_PRESS, + target={ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mocked_method = getattr(mock_automower_client.commands, "error_confirm") + mocked_method.assert_called_once_with(TEST_MOWER_ID) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == "2024-02-29T11:16:00+00:00" + getattr(mock_automower_client.commands, "error_confirm").side_effect = ApiException( + "Test error" + ) + with pytest.raises( + HomeAssistantError, + match="Failed to send command: Test error", + ): + await hass.services.async_call( + domain="button", + service=SERVICE_PRESS, + target={ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_button_snapshot( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Snapshot tests of the button entities.""" + with patch( + "homeassistant.components.husqvarna_automower.PLATFORMS", + [Platform.BUTTON], + ): + await setup_integration(hass, mock_config_entry) + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + )