Suppress roku power off timeout errors (#67414)

This commit is contained in:
Chris Talkington 2022-03-03 23:08:29 -06:00 committed by GitHub
parent d0bc5410cc
commit 79e9eb1b94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 70 additions and 59 deletions

View File

@ -1,14 +1,6 @@
"""Support for Roku."""
from __future__ import annotations
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
import logging
from typing import Any, TypeVar
from rokuecp import RokuConnectionError, RokuError
from typing_extensions import Concatenate, ParamSpec
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
@ -16,7 +8,6 @@ from homeassistant.helpers import config_validation as cv
from .const import DOMAIN
from .coordinator import RokuDataUpdateCoordinator
from .entity import RokuEntity
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
@ -27,10 +18,6 @@ PLATFORMS = [
Platform.SELECT,
Platform.SENSOR,
]
_LOGGER = logging.getLogger(__name__)
_T = TypeVar("_T", bound="RokuEntity")
_P = ParamSpec("_P")
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@ -53,22 +40,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
def roku_exception_handler(
func: Callable[Concatenate[_T, _P], Awaitable[None]] # type: ignore[misc]
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: # type: ignore[misc]
"""Decorate Roku calls to handle Roku exceptions."""
@wraps(func)
async def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
try:
await func(self, *args, **kwargs)
except RokuConnectionError as error:
if self.available:
_LOGGER.error("Error communicating with API: %s", error)
except RokuError as error:
if self.available:
_LOGGER.error("Invalid response from API: %s", error)
return wrapper

View File

@ -1,6 +1,21 @@
"""Helpers for Roku."""
from __future__ import annotations
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
import logging
from typing import Any, TypeVar
from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError
from typing_extensions import Concatenate, ParamSpec
from .entity import RokuEntity
_LOGGER = logging.getLogger(__name__)
_T = TypeVar("_T", bound=RokuEntity)
_P = ParamSpec("_P")
def format_channel_name(channel_number: str, channel_name: str | None = None) -> str:
"""Format a Roku Channel name."""
@ -8,3 +23,28 @@ def format_channel_name(channel_number: str, channel_name: str | None = None) ->
return f"{channel_name} ({channel_number})"
return channel_number
def roku_exception_handler(ignore_timeout: bool = False) -> Callable[..., Callable]:
"""Decorate Roku calls to handle Roku exceptions."""
def decorator(
func: Callable[Concatenate[_T, _P], Awaitable[None]], # type: ignore[misc]
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: # type: ignore[misc]
@wraps(func)
async def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
try:
await func(self, *args, **kwargs)
except RokuConnectionTimeoutError as error:
if not ignore_timeout and self.available:
_LOGGER.error("Error communicating with API: %s", error)
except RokuConnectionError as error:
if self.available:
_LOGGER.error("Error communicating with API: %s", error)
except RokuError as error:
if self.available:
_LOGGER.error("Invalid response from API: %s", error)
return wrapper
return decorator

View File

@ -2,7 +2,7 @@
"domain": "roku",
"name": "Roku",
"documentation": "https://www.home-assistant.io/integrations/roku",
"requirements": ["rokuecp==0.14.1"],
"requirements": ["rokuecp==0.15.0"],
"homekit": {
"models": ["3810X", "4660X", "7820X", "C105X", "C135X"]
},

View File

@ -51,7 +51,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_platform
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import roku_exception_handler
from .browse_media import async_browse_media
from .const import (
ATTR_ARTIST_NAME,
@ -65,7 +64,7 @@ from .const import (
)
from .coordinator import RokuDataUpdateCoordinator
from .entity import RokuEntity
from .helpers import format_channel_name
from .helpers import format_channel_name, roku_exception_handler
_LOGGER = logging.getLogger(__name__)
@ -289,7 +288,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
app.name for app in self.coordinator.data.apps if app.name is not None
)
@roku_exception_handler
@roku_exception_handler()
async def search(self, keyword: str) -> None:
"""Emulate opening the search screen and entering the search keyword."""
await self.coordinator.roku.search(keyword)
@ -321,68 +320,68 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
media_content_type,
)
@roku_exception_handler
@roku_exception_handler()
async def async_turn_on(self) -> None:
"""Turn on the Roku."""
await self.coordinator.roku.remote("poweron")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler(ignore_timeout=True)
async def async_turn_off(self) -> None:
"""Turn off the Roku."""
await self.coordinator.roku.remote("poweroff")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_pause(self) -> None:
"""Send pause command."""
if self.state not in (STATE_STANDBY, STATE_PAUSED):
await self.coordinator.roku.remote("play")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_play(self) -> None:
"""Send play command."""
if self.state not in (STATE_STANDBY, STATE_PLAYING):
await self.coordinator.roku.remote("play")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_play_pause(self) -> None:
"""Send play/pause command."""
if self.state != STATE_STANDBY:
await self.coordinator.roku.remote("play")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_previous_track(self) -> None:
"""Send previous track command."""
await self.coordinator.roku.remote("reverse")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_next_track(self) -> None:
"""Send next track command."""
await self.coordinator.roku.remote("forward")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_mute_volume(self, mute: bool) -> None:
"""Mute the volume."""
await self.coordinator.roku.remote("volume_mute")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_volume_up(self) -> None:
"""Volume up media player."""
await self.coordinator.roku.remote("volume_up")
@roku_exception_handler
@roku_exception_handler()
async def async_volume_down(self) -> None:
"""Volume down media player."""
await self.coordinator.roku.remote("volume_down")
@roku_exception_handler
@roku_exception_handler()
async def async_play_media(
self, media_type: str, media_id: str, **kwargs: Any
) -> None:
@ -487,7 +486,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_select_source(self, source: str) -> None:
"""Select input source."""
if source == "Home":

View File

@ -9,10 +9,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import roku_exception_handler
from .const import DOMAIN
from .coordinator import RokuDataUpdateCoordinator
from .entity import RokuEntity
from .helpers import roku_exception_handler
async def async_setup_entry(
@ -44,19 +44,19 @@ class RokuRemote(RokuEntity, RemoteEntity):
"""Return true if device is on."""
return not self.coordinator.data.state.standby
@roku_exception_handler
@roku_exception_handler()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
await self.coordinator.roku.remote("poweron")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler(ignore_timeout=True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
await self.coordinator.roku.remote("poweroff")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send a command to one device."""
num_repeats = kwargs[ATTR_NUM_REPEATS]

View File

@ -12,11 +12,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import roku_exception_handler
from .const import DOMAIN
from .coordinator import RokuDataUpdateCoordinator
from .entity import RokuEntity
from .helpers import format_channel_name
from .helpers import format_channel_name, roku_exception_handler
@dataclass
@ -163,7 +162,7 @@ class RokuSelectEntity(RokuEntity, SelectEntity):
"""Return a set of selectable options."""
return self.entity_description.options_fn(self.coordinator.data)
@roku_exception_handler
@roku_exception_handler()
async def async_select_option(self, option: str) -> None:
"""Set the option."""
await self.entity_description.set_fn(

View File

@ -2063,7 +2063,7 @@ rjpl==0.3.6
rocketchat-API==0.6.1
# homeassistant.components.roku
rokuecp==0.14.1
rokuecp==0.15.0
# homeassistant.components.roomba
roombapy==1.6.5

View File

@ -1309,7 +1309,7 @@ rflink==0.0.62
ring_doorbell==0.7.2
# homeassistant.components.roku
rokuecp==0.14.1
rokuecp==0.15.0
# homeassistant.components.roomba
roombapy==1.6.5

View File

@ -3,7 +3,7 @@ from datetime import timedelta
from unittest.mock import MagicMock, patch
import pytest
from rokuecp import RokuError
from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError
from homeassistant.components.media_player import MediaPlayerDeviceClass
from homeassistant.components.media_player.const import (
@ -164,10 +164,15 @@ async def test_tv_setup(
assert device_entry.suggested_area == "Living room"
@pytest.mark.parametrize(
"error",
[RokuConnectionTimeoutError, RokuConnectionError, RokuError],
)
async def test_availability(
hass: HomeAssistant,
mock_roku: MagicMock,
mock_config_entry: MockConfigEntry,
error: RokuError,
) -> None:
"""Test entity availability."""
now = dt_util.utcnow()
@ -179,7 +184,7 @@ async def test_availability(
await hass.async_block_till_done()
with patch("homeassistant.util.dt.utcnow", return_value=future):
mock_roku.update.side_effect = RokuError
mock_roku.update.side_effect = error
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
assert hass.states.get(MAIN_ENTITY_ID).state == STATE_UNAVAILABLE