mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 01:11:51 -04:00
Fix PJLink service errors breaking automation continue_on_error (#182186)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""Support for controlling projector via the PJLink protocol."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, override
|
||||
|
||||
import probatio
|
||||
@@ -16,6 +17,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
@@ -188,27 +190,32 @@ class PjLinkDevice(MediaPlayerEntity):
|
||||
else:
|
||||
raise
|
||||
|
||||
def _run_command(self, command: Callable[[Projector], None]) -> None:
|
||||
"""Run a projector command and surface failures as HomeAssistantError."""
|
||||
try:
|
||||
with self.projector() as projector:
|
||||
command(projector)
|
||||
except (ProjectorError, TimeoutError, OSError) as err:
|
||||
# script helpers honor continue_on_error only for HomeAssistantError
|
||||
raise HomeAssistantError(f"PJLink command failed: {err}") from err
|
||||
|
||||
@override
|
||||
def turn_off(self) -> None:
|
||||
"""Turn projector off."""
|
||||
with self.projector() as projector:
|
||||
projector.set_power("off")
|
||||
self._run_command(lambda projector: projector.set_power("off"))
|
||||
|
||||
@override
|
||||
def turn_on(self) -> None:
|
||||
"""Turn projector on."""
|
||||
with self.projector() as projector:
|
||||
projector.set_power("on")
|
||||
self._run_command(lambda projector: projector.set_power("on"))
|
||||
|
||||
@override
|
||||
def mute_volume(self, mute: bool) -> None:
|
||||
"""Mute (true) of unmute (false) media player."""
|
||||
with self.projector() as projector:
|
||||
projector.set_mute(MUTE_AUDIO, mute)
|
||||
self._run_command(lambda projector: projector.set_mute(MUTE_AUDIO, mute))
|
||||
|
||||
@override
|
||||
def select_source(self, source: str) -> None:
|
||||
"""Set the input source."""
|
||||
source = self._source_name_mapping[source]
|
||||
with self.projector() as projector:
|
||||
projector.set_input(*source)
|
||||
selected = self._source_name_mapping[source]
|
||||
self._run_command(lambda projector: projector.set_input(*selected))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from datetime import timedelta
|
||||
import socket
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, create_autospec, patch
|
||||
|
||||
import pypjlink
|
||||
@@ -25,6 +26,7 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -322,3 +324,84 @@ async def test_failed_yaml_import(
|
||||
assert issue_registry.async_get_issue(
|
||||
DOMAIN, f"deprecated_yaml_import_issue_{error_str}"
|
||||
)
|
||||
|
||||
|
||||
_ERROR = ProjectorError("projector error")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "service_data", "method_name"),
|
||||
[
|
||||
pytest.param("turn_off", {}, "set_power", id="turn_off"),
|
||||
pytest.param("turn_on", {}, "set_power", id="turn_on"),
|
||||
pytest.param(
|
||||
"volume_mute", {"is_volume_muted": True}, "set_mute", id="volume_mute"
|
||||
),
|
||||
pytest.param(
|
||||
"select_source", {"source": "VGA 1"}, "set_input", id="select_source"
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_service_command_error_is_home_assistant_error(
|
||||
mocked_projector: MagicMock,
|
||||
hass: HomeAssistant,
|
||||
service: str,
|
||||
service_data: dict[str, Any],
|
||||
method_name: str,
|
||||
) -> None:
|
||||
"""Projector failures during a service command surface as HomeAssistantError."""
|
||||
|
||||
getattr(mocked_projector, method_name).side_effect = _ERROR
|
||||
|
||||
await setup_pjlink_entry(hass)
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
media_player.DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: "media_player.test", **service_data},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_service_error_when_projector_unreachable_is_home_assistant_error(
|
||||
mocked_projector: MagicMock,
|
||||
projector_from_address: MagicMock,
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""A command on an unreachable projector surfaces as HomeAssistantError."""
|
||||
|
||||
await setup_pjlink_entry(hass)
|
||||
|
||||
projector_from_address.side_effect = socket.timeout
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
media_player.DOMAIN,
|
||||
"turn_off",
|
||||
{ATTR_ENTITY_ID: "media_player.test"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"socket_error", [TimeoutError, OSError], ids=["timeout", "os_error"]
|
||||
)
|
||||
async def test_service_socket_error_during_command_is_home_assistant_error(
|
||||
mocked_projector: MagicMock,
|
||||
hass: HomeAssistant,
|
||||
socket_error: type[Exception],
|
||||
) -> None:
|
||||
"""A socket error raised by the command itself surfaces as HomeAssistantError."""
|
||||
|
||||
mocked_projector.set_power.side_effect = socket_error
|
||||
|
||||
await setup_pjlink_entry(hass)
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
media_player.DOMAIN,
|
||||
"turn_off",
|
||||
{ATTR_ENTITY_ID: "media_player.test"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user