mirror of
https://github.com/home-assistant/core.git
synced 2025-07-10 14:57:09 +00:00
Suppress roku power off timeout errors (#67414)
This commit is contained in:
parent
d0bc5410cc
commit
79e9eb1b94
@ -1,14 +1,6 @@
|
|||||||
"""Support for Roku."""
|
"""Support for Roku."""
|
||||||
from __future__ import annotations
|
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.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, Platform
|
from homeassistant.const import CONF_HOST, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@ -16,7 +8,6 @@ from homeassistant.helpers import config_validation as cv
|
|||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import RokuDataUpdateCoordinator
|
from .coordinator import RokuDataUpdateCoordinator
|
||||||
from .entity import RokuEntity
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
||||||
|
|
||||||
@ -27,10 +18,6 @@ PLATFORMS = [
|
|||||||
Platform.SELECT,
|
Platform.SELECT,
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
]
|
]
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_T = TypeVar("_T", bound="RokuEntity")
|
|
||||||
_P = ParamSpec("_P")
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
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:
|
if unload_ok:
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
return unload_ok
|
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
|
|
||||||
|
@ -1,6 +1,21 @@
|
|||||||
"""Helpers for Roku."""
|
"""Helpers for Roku."""
|
||||||
from __future__ import annotations
|
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:
|
def format_channel_name(channel_number: str, channel_name: str | None = None) -> str:
|
||||||
"""Format a Roku Channel name."""
|
"""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 f"{channel_name} ({channel_number})"
|
||||||
|
|
||||||
return 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
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "roku",
|
"domain": "roku",
|
||||||
"name": "Roku",
|
"name": "Roku",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/roku",
|
"documentation": "https://www.home-assistant.io/integrations/roku",
|
||||||
"requirements": ["rokuecp==0.14.1"],
|
"requirements": ["rokuecp==0.15.0"],
|
||||||
"homekit": {
|
"homekit": {
|
||||||
"models": ["3810X", "4660X", "7820X", "C105X", "C135X"]
|
"models": ["3810X", "4660X", "7820X", "C105X", "C135X"]
|
||||||
},
|
},
|
||||||
|
@ -51,7 +51,6 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers import entity_platform
|
from homeassistant.helpers import entity_platform
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import roku_exception_handler
|
|
||||||
from .browse_media import async_browse_media
|
from .browse_media import async_browse_media
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_ARTIST_NAME,
|
ATTR_ARTIST_NAME,
|
||||||
@ -65,7 +64,7 @@ from .const import (
|
|||||||
)
|
)
|
||||||
from .coordinator import RokuDataUpdateCoordinator
|
from .coordinator import RokuDataUpdateCoordinator
|
||||||
from .entity import RokuEntity
|
from .entity import RokuEntity
|
||||||
from .helpers import format_channel_name
|
from .helpers import format_channel_name, roku_exception_handler
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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
|
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:
|
async def search(self, keyword: str) -> None:
|
||||||
"""Emulate opening the search screen and entering the search keyword."""
|
"""Emulate opening the search screen and entering the search keyword."""
|
||||||
await self.coordinator.roku.search(keyword)
|
await self.coordinator.roku.search(keyword)
|
||||||
@ -321,68 +320,68 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
|||||||
media_content_type,
|
media_content_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
@roku_exception_handler
|
@roku_exception_handler()
|
||||||
async def async_turn_on(self) -> None:
|
async def async_turn_on(self) -> None:
|
||||||
"""Turn on the Roku."""
|
"""Turn on the Roku."""
|
||||||
await self.coordinator.roku.remote("poweron")
|
await self.coordinator.roku.remote("poweron")
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
@roku_exception_handler
|
@roku_exception_handler(ignore_timeout=True)
|
||||||
async def async_turn_off(self) -> None:
|
async def async_turn_off(self) -> None:
|
||||||
"""Turn off the Roku."""
|
"""Turn off the Roku."""
|
||||||
await self.coordinator.roku.remote("poweroff")
|
await self.coordinator.roku.remote("poweroff")
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
@roku_exception_handler
|
@roku_exception_handler()
|
||||||
async def async_media_pause(self) -> None:
|
async def async_media_pause(self) -> None:
|
||||||
"""Send pause command."""
|
"""Send pause command."""
|
||||||
if self.state not in (STATE_STANDBY, STATE_PAUSED):
|
if self.state not in (STATE_STANDBY, STATE_PAUSED):
|
||||||
await self.coordinator.roku.remote("play")
|
await self.coordinator.roku.remote("play")
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
@roku_exception_handler
|
@roku_exception_handler()
|
||||||
async def async_media_play(self) -> None:
|
async def async_media_play(self) -> None:
|
||||||
"""Send play command."""
|
"""Send play command."""
|
||||||
if self.state not in (STATE_STANDBY, STATE_PLAYING):
|
if self.state not in (STATE_STANDBY, STATE_PLAYING):
|
||||||
await self.coordinator.roku.remote("play")
|
await self.coordinator.roku.remote("play")
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
@roku_exception_handler
|
@roku_exception_handler()
|
||||||
async def async_media_play_pause(self) -> None:
|
async def async_media_play_pause(self) -> None:
|
||||||
"""Send play/pause command."""
|
"""Send play/pause command."""
|
||||||
if self.state != STATE_STANDBY:
|
if self.state != STATE_STANDBY:
|
||||||
await self.coordinator.roku.remote("play")
|
await self.coordinator.roku.remote("play")
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
@roku_exception_handler
|
@roku_exception_handler()
|
||||||
async def async_media_previous_track(self) -> None:
|
async def async_media_previous_track(self) -> None:
|
||||||
"""Send previous track command."""
|
"""Send previous track command."""
|
||||||
await self.coordinator.roku.remote("reverse")
|
await self.coordinator.roku.remote("reverse")
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
@roku_exception_handler
|
@roku_exception_handler()
|
||||||
async def async_media_next_track(self) -> None:
|
async def async_media_next_track(self) -> None:
|
||||||
"""Send next track command."""
|
"""Send next track command."""
|
||||||
await self.coordinator.roku.remote("forward")
|
await self.coordinator.roku.remote("forward")
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
@roku_exception_handler
|
@roku_exception_handler()
|
||||||
async def async_mute_volume(self, mute: bool) -> None:
|
async def async_mute_volume(self, mute: bool) -> None:
|
||||||
"""Mute the volume."""
|
"""Mute the volume."""
|
||||||
await self.coordinator.roku.remote("volume_mute")
|
await self.coordinator.roku.remote("volume_mute")
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
@roku_exception_handler
|
@roku_exception_handler()
|
||||||
async def async_volume_up(self) -> None:
|
async def async_volume_up(self) -> None:
|
||||||
"""Volume up media player."""
|
"""Volume up media player."""
|
||||||
await self.coordinator.roku.remote("volume_up")
|
await self.coordinator.roku.remote("volume_up")
|
||||||
|
|
||||||
@roku_exception_handler
|
@roku_exception_handler()
|
||||||
async def async_volume_down(self) -> None:
|
async def async_volume_down(self) -> None:
|
||||||
"""Volume down media player."""
|
"""Volume down media player."""
|
||||||
await self.coordinator.roku.remote("volume_down")
|
await self.coordinator.roku.remote("volume_down")
|
||||||
|
|
||||||
@roku_exception_handler
|
@roku_exception_handler()
|
||||||
async def async_play_media(
|
async def async_play_media(
|
||||||
self, media_type: str, media_id: str, **kwargs: Any
|
self, media_type: str, media_id: str, **kwargs: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -487,7 +486,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
|||||||
|
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
@roku_exception_handler
|
@roku_exception_handler()
|
||||||
async def async_select_source(self, source: str) -> None:
|
async def async_select_source(self, source: str) -> None:
|
||||||
"""Select input source."""
|
"""Select input source."""
|
||||||
if source == "Home":
|
if source == "Home":
|
||||||
|
@ -9,10 +9,10 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import roku_exception_handler
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import RokuDataUpdateCoordinator
|
from .coordinator import RokuDataUpdateCoordinator
|
||||||
from .entity import RokuEntity
|
from .entity import RokuEntity
|
||||||
|
from .helpers import roku_exception_handler
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@ -44,19 +44,19 @@ class RokuRemote(RokuEntity, RemoteEntity):
|
|||||||
"""Return true if device is on."""
|
"""Return true if device is on."""
|
||||||
return not self.coordinator.data.state.standby
|
return not self.coordinator.data.state.standby
|
||||||
|
|
||||||
@roku_exception_handler
|
@roku_exception_handler()
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
await self.coordinator.roku.remote("poweron")
|
await self.coordinator.roku.remote("poweron")
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
@roku_exception_handler
|
@roku_exception_handler(ignore_timeout=True)
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
await self.coordinator.roku.remote("poweroff")
|
await self.coordinator.roku.remote("poweroff")
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
@roku_exception_handler
|
@roku_exception_handler()
|
||||||
async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
|
async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
|
||||||
"""Send a command to one device."""
|
"""Send a command to one device."""
|
||||||
num_repeats = kwargs[ATTR_NUM_REPEATS]
|
num_repeats = kwargs[ATTR_NUM_REPEATS]
|
||||||
|
@ -12,11 +12,10 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import roku_exception_handler
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import RokuDataUpdateCoordinator
|
from .coordinator import RokuDataUpdateCoordinator
|
||||||
from .entity import RokuEntity
|
from .entity import RokuEntity
|
||||||
from .helpers import format_channel_name
|
from .helpers import format_channel_name, roku_exception_handler
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -163,7 +162,7 @@ class RokuSelectEntity(RokuEntity, SelectEntity):
|
|||||||
"""Return a set of selectable options."""
|
"""Return a set of selectable options."""
|
||||||
return self.entity_description.options_fn(self.coordinator.data)
|
return self.entity_description.options_fn(self.coordinator.data)
|
||||||
|
|
||||||
@roku_exception_handler
|
@roku_exception_handler()
|
||||||
async def async_select_option(self, option: str) -> None:
|
async def async_select_option(self, option: str) -> None:
|
||||||
"""Set the option."""
|
"""Set the option."""
|
||||||
await self.entity_description.set_fn(
|
await self.entity_description.set_fn(
|
||||||
|
@ -2063,7 +2063,7 @@ rjpl==0.3.6
|
|||||||
rocketchat-API==0.6.1
|
rocketchat-API==0.6.1
|
||||||
|
|
||||||
# homeassistant.components.roku
|
# homeassistant.components.roku
|
||||||
rokuecp==0.14.1
|
rokuecp==0.15.0
|
||||||
|
|
||||||
# homeassistant.components.roomba
|
# homeassistant.components.roomba
|
||||||
roombapy==1.6.5
|
roombapy==1.6.5
|
||||||
|
@ -1309,7 +1309,7 @@ rflink==0.0.62
|
|||||||
ring_doorbell==0.7.2
|
ring_doorbell==0.7.2
|
||||||
|
|
||||||
# homeassistant.components.roku
|
# homeassistant.components.roku
|
||||||
rokuecp==0.14.1
|
rokuecp==0.15.0
|
||||||
|
|
||||||
# homeassistant.components.roomba
|
# homeassistant.components.roomba
|
||||||
roombapy==1.6.5
|
roombapy==1.6.5
|
||||||
|
@ -3,7 +3,7 @@ from datetime import timedelta
|
|||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from rokuecp import RokuError
|
from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError
|
||||||
|
|
||||||
from homeassistant.components.media_player import MediaPlayerDeviceClass
|
from homeassistant.components.media_player import MediaPlayerDeviceClass
|
||||||
from homeassistant.components.media_player.const import (
|
from homeassistant.components.media_player.const import (
|
||||||
@ -164,10 +164,15 @@ async def test_tv_setup(
|
|||||||
assert device_entry.suggested_area == "Living room"
|
assert device_entry.suggested_area == "Living room"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"error",
|
||||||
|
[RokuConnectionTimeoutError, RokuConnectionError, RokuError],
|
||||||
|
)
|
||||||
async def test_availability(
|
async def test_availability(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_roku: MagicMock,
|
mock_roku: MagicMock,
|
||||||
mock_config_entry: MockConfigEntry,
|
mock_config_entry: MockConfigEntry,
|
||||||
|
error: RokuError,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test entity availability."""
|
"""Test entity availability."""
|
||||||
now = dt_util.utcnow()
|
now = dt_util.utcnow()
|
||||||
@ -179,7 +184,7 @@ async def test_availability(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
with patch("homeassistant.util.dt.utcnow", return_value=future):
|
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)
|
async_fire_time_changed(hass, future)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert hass.states.get(MAIN_ENTITY_ID).state == STATE_UNAVAILABLE
|
assert hass.states.get(MAIN_ENTITY_ID).state == STATE_UNAVAILABLE
|
||||||
|
Loading…
x
Reference in New Issue
Block a user