From 4fb062102754b27c20dc96f349584ab2910d21e2 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Fri, 28 Jun 2024 20:11:03 +0200 Subject: [PATCH] Catch exceptions in service calls by buttons/switches in pyLoad integration (#120701) * Catch exceptions in service calls by buttons/switches * changes * more changes * update tests --- homeassistant/components/pyload/button.py | 17 ++++++- homeassistant/components/pyload/strings.json | 6 +++ homeassistant/components/pyload/switch.py | 46 +++++++++++++++++-- tests/components/pyload/test_button.py | 41 ++++++++++++++++- tests/components/pyload/test_switch.py | 48 ++++++++++++++++++++ 5 files changed, 151 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/pyload/button.py b/homeassistant/components/pyload/button.py index 950177f8751..386fe6968de 100644 --- a/homeassistant/components/pyload/button.py +++ b/homeassistant/components/pyload/button.py @@ -7,13 +7,15 @@ from dataclasses import dataclass from enum import StrEnum from typing import Any -from pyloadapi.api import PyLoadAPI +from pyloadapi.api import CannotConnect, InvalidAuth, PyLoadAPI from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import PyLoadConfigEntry +from .const import DOMAIN from .entity import BasePyLoadEntity @@ -80,4 +82,15 @@ class PyLoadBinarySensor(BasePyLoadEntity, ButtonEntity): async def async_press(self) -> None: """Handle the button press.""" - await self.entity_description.press_fn(self.coordinator.pyload) + try: + await self.entity_description.press_fn(self.coordinator.pyload) + except CannotConnect as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_exception", + ) from e + except InvalidAuth as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_auth_exception", + ) from e diff --git a/homeassistant/components/pyload/strings.json b/homeassistant/components/pyload/strings.json index 9fe311574fb..38e17e5016f 100644 --- a/homeassistant/components/pyload/strings.json +++ b/homeassistant/components/pyload/strings.json @@ -98,6 +98,12 @@ }, "setup_authentication_exception": { "message": "Authentication failed for {username}, verify your login credentials" + }, + "service_call_exception": { + "message": "Unable to send command to pyLoad due to a connection error, try again later" + }, + "service_call_auth_exception": { + "message": "Unable to send command to pyLoad due to an authentication error, try again later" } }, "issues": { diff --git a/homeassistant/components/pyload/switch.py b/homeassistant/components/pyload/switch.py index 5e8c61823dd..ea189ed9a8f 100644 --- a/homeassistant/components/pyload/switch.py +++ b/homeassistant/components/pyload/switch.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from enum import StrEnum from typing import Any -from pyloadapi.api import PyLoadAPI +from pyloadapi.api import CannotConnect, InvalidAuth, PyLoadAPI from homeassistant.components.switch import ( SwitchDeviceClass, @@ -15,9 +15,11 @@ from homeassistant.components.switch import ( SwitchEntityDescription, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import PyLoadConfigEntry +from .const import DOMAIN from .coordinator import PyLoadData from .entity import BasePyLoadEntity @@ -90,15 +92,51 @@ class PyLoadSwitchEntity(BasePyLoadEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - await self.entity_description.turn_on_fn(self.coordinator.pyload) + try: + await self.entity_description.turn_on_fn(self.coordinator.pyload) + except CannotConnect as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_exception", + ) from e + except InvalidAuth as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_auth_exception", + ) from e + await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity on.""" - await self.entity_description.turn_off_fn(self.coordinator.pyload) + try: + await self.entity_description.turn_off_fn(self.coordinator.pyload) + except CannotConnect as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_exception", + ) from e + except InvalidAuth as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_auth_exception", + ) from e + await self.coordinator.async_refresh() async def async_toggle(self, **kwargs: Any) -> None: """Toggle the entity.""" - await self.entity_description.toggle_fn(self.coordinator.pyload) + try: + await self.entity_description.toggle_fn(self.coordinator.pyload) + except CannotConnect as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_exception", + ) from e + except InvalidAuth as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_auth_exception", + ) from e + await self.coordinator.async_refresh() diff --git a/tests/components/pyload/test_button.py b/tests/components/pyload/test_button.py index b5aa18ad3d9..53f592374ba 100644 --- a/tests/components/pyload/test_button.py +++ b/tests/components/pyload/test_button.py @@ -3,6 +3,7 @@ from collections.abc import AsyncGenerator from unittest.mock import AsyncMock, call, patch +from pyloadapi import CannotConnect, InvalidAuth import pytest from syrupy.assertion import SnapshotAssertion @@ -11,6 +12,7 @@ from homeassistant.components.pyload.button import PyLoadButtonEntity from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry, snapshot_platform @@ -78,6 +80,43 @@ async def test_button_press( {ATTR_ENTITY_ID: entity_entry.entity_id}, blocking=True, ) - await hass.async_block_till_done() assert API_CALL[entity_entry.translation_key] in mock_pyloadapi.method_calls mock_pyloadapi.reset_mock() + + +@pytest.mark.parametrize( + ("side_effect"), + [CannotConnect, InvalidAuth], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_button_press_errors( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, + entity_registry: er.EntityRegistry, + side_effect: Exception, +) -> None: + """Test button press method.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + entity_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + mock_pyloadapi.stop_all_downloads.side_effect = side_effect + mock_pyloadapi.restart_failed.side_effect = side_effect + mock_pyloadapi.delete_finished.side_effect = side_effect + mock_pyloadapi.restart.side_effect = side_effect + + for entity_entry in entity_entries: + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_entry.entity_id}, + blocking=True, + ) diff --git a/tests/components/pyload/test_switch.py b/tests/components/pyload/test_switch.py index 42a6bfa6f14..8e99cb00cfe 100644 --- a/tests/components/pyload/test_switch.py +++ b/tests/components/pyload/test_switch.py @@ -3,6 +3,7 @@ from collections.abc import AsyncGenerator from unittest.mock import AsyncMock, call, patch +from pyloadapi import CannotConnect, InvalidAuth import pytest from syrupy.assertion import SnapshotAssertion @@ -16,6 +17,7 @@ from homeassistant.components.switch import ( from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry, snapshot_platform @@ -102,3 +104,49 @@ async def test_turn_on_off( in mock_pyloadapi.method_calls ) mock_pyloadapi.reset_mock() + + +@pytest.mark.parametrize( + ("service_call"), + [ + SERVICE_TURN_ON, + SERVICE_TURN_OFF, + SERVICE_TOGGLE, + ], +) +@pytest.mark.parametrize( + ("side_effect"), + [CannotConnect, InvalidAuth], +) +async def test_turn_on_off_errors( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pyloadapi: AsyncMock, + service_call: str, + entity_registry: er.EntityRegistry, + side_effect: Exception, +) -> None: + """Test switch turn on/off, toggle method.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + entity_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + mock_pyloadapi.unpause.side_effect = side_effect + mock_pyloadapi.pause.side_effect = side_effect + mock_pyloadapi.toggle_pause.side_effect = side_effect + mock_pyloadapi.toggle_reconnect.side_effect = side_effect + + for entity_entry in entity_entries: + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + SWITCH_DOMAIN, + service_call, + {ATTR_ENTITY_ID: entity_entry.entity_id}, + blocking=True, + )