Skip to content

Commit

Permalink
Catch exceptions in service calls by buttons/switches in pyLoad integ…
Browse files Browse the repository at this point in the history
…ration (home-assistant#120701)

* Catch exceptions in service calls by buttons/switches

* changes

* more changes

* update tests
  • Loading branch information
tr4nt0r authored Jun 28, 2024
1 parent c029c53 commit 4fb0621
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 7 deletions.
17 changes: 15 additions & 2 deletions homeassistant/components/pyload/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
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


Expand Down Expand Up @@ -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
6 changes: 6 additions & 0 deletions homeassistant/components/pyload/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
46 changes: 42 additions & 4 deletions homeassistant/components/pyload/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@
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,
SwitchEntity,
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

Expand Down Expand Up @@ -90,15 +92,51 @@ def is_on(self) -> bool | None:

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()
41 changes: 40 additions & 1 deletion tests/components/pyload/test_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -11,6 +12,7 @@
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
Expand Down Expand Up @@ -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,
)
48 changes: 48 additions & 0 deletions tests/components/pyload/test_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -16,6 +17,7 @@
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
Expand Down Expand Up @@ -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,
)

0 comments on commit 4fb0621

Please sign in to comment.