Make roku async (#35104)
* Update manifest.json * work on roku async. * Update config_flow.py * Update __init__.py * Update media_player.py * Update media_player.py * Update __init__.py * Update __init__.py * Update media_player.py * Update media_player.py * Update remote.py * Update test_media_player.py * Update test_media_player.py * Update test_config_flow.py * Update media_player.py * Update remote.py * Update config_flow.py * Update test_media_player.py * Update config_flow.py * Update test_config_flow.py
This commit is contained in:
parent
54584e970c
commit
3feb55a8e4
16 changed files with 715 additions and 490 deletions
|
@ -1,22 +1,31 @@
|
|||
"""Support for Roku."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from socket import gaierror as SocketGIAError
|
||||
from typing import Dict
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from requests.exceptions import RequestException
|
||||
from roku import Roku, RokuException
|
||||
from rokuecp import Roku, RokuError
|
||||
from rokuecp.models import Device
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||
from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import ATTR_NAME, CONF_HOST
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DATA_CLIENT, DATA_DEVICE_INFO, DOMAIN
|
||||
from .const import (
|
||||
ATTR_IDENTIFIERS,
|
||||
ATTR_MANUFACTURER,
|
||||
ATTR_MODEL,
|
||||
ATTR_SOFTWARE_VERSION,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
|
@ -29,20 +38,10 @@ CONFIG_SCHEMA = vol.Schema(
|
|||
|
||||
PLATFORMS = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN]
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_roku_data(host: str) -> dict:
|
||||
"""Retrieve a Roku instance and version info for the device."""
|
||||
roku = Roku(host)
|
||||
roku_device_info = roku.device_info
|
||||
|
||||
return {
|
||||
DATA_CLIENT: roku,
|
||||
DATA_DEVICE_INFO: roku_device_info,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: Dict) -> bool:
|
||||
async def async_setup(hass: HomeAssistantType, config: Dict) -> bool:
|
||||
"""Set up the Roku integration."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
|
@ -57,16 +56,15 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool:
|
|||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
|
||||
"""Set up Roku from a config entry."""
|
||||
try:
|
||||
roku_data = await hass.async_add_executor_job(
|
||||
get_roku_data, entry.data[CONF_HOST],
|
||||
)
|
||||
except (SocketGIAError, RequestException, RokuException) as exception:
|
||||
raise ConfigEntryNotReady from exception
|
||||
coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST])
|
||||
await coordinator.async_refresh()
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = roku_data
|
||||
if not coordinator.last_update_success:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
|
@ -76,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
|
@ -91,3 +89,75 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
class RokuDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to manage fetching Roku data."""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistantType, *, host: str,
|
||||
):
|
||||
"""Initialize global Roku data updater."""
|
||||
self.roku = Roku(host=host, session=async_get_clientsession(hass))
|
||||
|
||||
super().__init__(
|
||||
hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> Device:
|
||||
"""Fetch data from Roku."""
|
||||
try:
|
||||
return await self.roku.update()
|
||||
except RokuError as error:
|
||||
raise UpdateFailed(f"Invalid response from API: {error}")
|
||||
|
||||
|
||||
class RokuEntity(Entity):
|
||||
"""Defines a base Roku entity."""
|
||||
|
||||
def __init__(
|
||||
self, *, device_id: str, name: str, coordinator: RokuDataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Initialize the Roku entity."""
|
||||
self._device_id = device_id
|
||||
self._name = name
|
||||
self.coordinator = coordinator
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self.coordinator.last_update_success
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return the polling requirement of the entity."""
|
||||
return False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Connect to dispatcher listening for entity data notifications."""
|
||||
self.async_on_remove(
|
||||
self.coordinator.async_add_listener(self.async_write_ha_state)
|
||||
)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update an Roku entity."""
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@property
|
||||
def device_info(self) -> Dict[str, Any]:
|
||||
"""Return device information about this Roku device."""
|
||||
if self._device_id is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)},
|
||||
ATTR_NAME: self.name,
|
||||
ATTR_MANUFACTURER: self.coordinator.data.info.brand,
|
||||
ATTR_MODEL: self.coordinator.data.info.model_name,
|
||||
ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version,
|
||||
}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
"""Config flow for Roku."""
|
||||
import logging
|
||||
from socket import gaierror as SocketGIAError
|
||||
from typing import Any, Dict, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from requests.exceptions import RequestException
|
||||
from roku import Roku, RokuException
|
||||
from rokuecp import Roku, RokuError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.ssdp import (
|
||||
|
@ -16,7 +14,8 @@ from homeassistant.components.ssdp import (
|
|||
from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from .const import DOMAIN # pylint: disable=unused-import
|
||||
|
||||
|
@ -28,24 +27,18 @@ ERROR_UNKNOWN = "unknown"
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def validate_input(data: Dict) -> Dict:
|
||||
async def validate_input(hass: HomeAssistantType, data: Dict) -> Dict:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
|
||||
try:
|
||||
roku = Roku(data["host"])
|
||||
device_info = roku.device_info
|
||||
except (SocketGIAError, RequestException, RokuException) as exception:
|
||||
raise CannotConnect from exception
|
||||
except Exception as exception: # pylint: disable=broad-except
|
||||
raise UnknownError from exception
|
||||
session = async_get_clientsession(hass)
|
||||
roku = Roku(data[CONF_HOST], session=session)
|
||||
device = await roku.update()
|
||||
|
||||
return {
|
||||
"title": data["host"],
|
||||
"host": data["host"],
|
||||
"serial_num": device_info.serial_num,
|
||||
"title": device.info.name,
|
||||
"serial_number": device.info.serial_number,
|
||||
}
|
||||
|
||||
|
||||
|
@ -55,6 +48,10 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
VERSION = 1
|
||||
CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Set up the instance."""
|
||||
self.discovery_info = {}
|
||||
|
||||
@callback
|
||||
def _show_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""Show the form to the user."""
|
||||
|
@ -78,16 +75,17 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
errors = {}
|
||||
|
||||
try:
|
||||
info = await self.hass.async_add_executor_job(validate_input, user_input)
|
||||
except CannotConnect:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
except RokuError:
|
||||
_LOGGER.debug("Roku Error", exc_info=True)
|
||||
errors["base"] = ERROR_CANNOT_CONNECT
|
||||
return self._show_form(errors)
|
||||
except UnknownError:
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unknown error trying to connect")
|
||||
return self.async_abort(reason=ERROR_UNKNOWN)
|
||||
|
||||
await self.async_set_unique_id(info["serial_num"])
|
||||
self._abort_if_unique_id_configured()
|
||||
await self.async_set_unique_id(info["serial_number"])
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]})
|
||||
|
||||
return self.async_create_entry(title=info["title"], data=user_input)
|
||||
|
||||
|
@ -97,15 +95,24 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
"""Handle a flow initialized by discovery."""
|
||||
host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname
|
||||
name = discovery_info[ATTR_UPNP_FRIENDLY_NAME]
|
||||
serial_num = discovery_info[ATTR_UPNP_SERIAL]
|
||||
serial_number = discovery_info[ATTR_UPNP_SERIAL]
|
||||
|
||||
await self.async_set_unique_id(serial_num)
|
||||
await self.async_set_unique_id(serial_number)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
|
||||
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
self.context.update(
|
||||
{CONF_HOST: host, CONF_NAME: name, "title_placeholders": {"name": host}}
|
||||
)
|
||||
self.context.update({"title_placeholders": {"name": name}})
|
||||
|
||||
self.discovery_info.update({CONF_HOST: host, CONF_NAME: name})
|
||||
|
||||
try:
|
||||
await validate_input(self.hass, self.discovery_info)
|
||||
except RokuError:
|
||||
_LOGGER.debug("Roku Error", exc_info=True)
|
||||
return self.async_abort(reason=ERROR_CANNOT_CONNECT)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unknown error trying to connect")
|
||||
return self.async_abort(reason=ERROR_UNKNOWN)
|
||||
|
||||
return await self.async_step_ssdp_confirm()
|
||||
|
||||
|
@ -114,30 +121,13 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
) -> Dict[str, Any]:
|
||||
"""Handle user-confirmation of discovered device."""
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
name = self.context.get(CONF_NAME)
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="ssdp_confirm",
|
||||
description_placeholders={"name": self.discovery_info[CONF_NAME]},
|
||||
errors={},
|
||||
)
|
||||
|
||||
if user_input is not None:
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
user_input[CONF_HOST] = self.context.get(CONF_HOST)
|
||||
user_input[CONF_NAME] = name
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(validate_input, user_input)
|
||||
return self.async_create_entry(title=name, data=user_input)
|
||||
except CannotConnect:
|
||||
return self.async_abort(reason=ERROR_CANNOT_CONNECT)
|
||||
except UnknownError:
|
||||
_LOGGER.exception("Unknown error trying to connect")
|
||||
return self.async_abort(reason=ERROR_UNKNOWN)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="ssdp_confirm", description_placeholders={"name": name},
|
||||
return self.async_create_entry(
|
||||
title=self.discovery_info[CONF_NAME], data=self.discovery_info,
|
||||
)
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class UnknownError(HomeAssistantError):
|
||||
"""Error to indicate we encountered an unknown error."""
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
"""Constants for the Roku integration."""
|
||||
DOMAIN = "roku"
|
||||
|
||||
DATA_CLIENT = "client"
|
||||
DATA_DEVICE_INFO = "device_info"
|
||||
# Attributes
|
||||
ATTR_IDENTIFIERS = "identifiers"
|
||||
ATTR_MANUFACTURER = "manufacturer"
|
||||
ATTR_MODEL = "model"
|
||||
ATTR_SOFTWARE_VERSION = "sw_version"
|
||||
|
||||
# Default Values
|
||||
DEFAULT_PORT = 8060
|
||||
DEFAULT_MANUFACTURER = "Roku"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"domain": "roku",
|
||||
"name": "Roku",
|
||||
"documentation": "https://www.home-assistant.io/integrations/roku",
|
||||
"requirements": ["roku==4.1.0"],
|
||||
"requirements": ["rokuecp==0.2.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "roku:ecp",
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
"""Support for the Roku media player."""
|
||||
import logging
|
||||
|
||||
from requests.exceptions import (
|
||||
ConnectionError as RequestsConnectionError,
|
||||
ReadTimeout as RequestsReadTimeout,
|
||||
)
|
||||
from roku import RokuException
|
||||
from typing import List
|
||||
|
||||
from homeassistant.components.media_player import MediaPlayerEntity
|
||||
from homeassistant.components.media_player.const import (
|
||||
MEDIA_TYPE_APP,
|
||||
MEDIA_TYPE_CHANNEL,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PLAY,
|
||||
|
@ -22,7 +18,8 @@ from homeassistant.components.media_player.const import (
|
|||
)
|
||||
from homeassistant.const import STATE_HOME, STATE_IDLE, STATE_PLAYING, STATE_STANDBY
|
||||
|
||||
from .const import DATA_CLIENT, DEFAULT_MANUFACTURER, DEFAULT_PORT, DOMAIN
|
||||
from . import RokuDataUpdateCoordinator, RokuEntity
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -41,68 +38,48 @@ SUPPORT_ROKU = (
|
|||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up the Roku config entry."""
|
||||
roku = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT]
|
||||
async_add_entities([RokuDevice(roku)], True)
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
unique_id = coordinator.data.info.serial_number
|
||||
async_add_entities([RokuMediaPlayer(unique_id, coordinator)], True)
|
||||
|
||||
|
||||
class RokuDevice(MediaPlayerEntity):
|
||||
"""Representation of a Roku device on the network."""
|
||||
class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
||||
"""Representation of a Roku media player on the network."""
|
||||
|
||||
def __init__(self, roku):
|
||||
def __init__(self, unique_id: str, coordinator: RokuDataUpdateCoordinator) -> None:
|
||||
"""Initialize the Roku device."""
|
||||
self.roku = roku
|
||||
self.ip_address = roku.host
|
||||
self.channels = []
|
||||
self.current_app = None
|
||||
self._available = False
|
||||
self._device_info = {}
|
||||
self._power_state = "Unknown"
|
||||
super().__init__(
|
||||
coordinator=coordinator,
|
||||
name=coordinator.data.info.name,
|
||||
device_id=unique_id,
|
||||
)
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
try:
|
||||
self._device_info = self.roku.device_info
|
||||
self._power_state = self.roku.power_state
|
||||
self.ip_address = self.roku.host
|
||||
self.channels = self.get_source_list()
|
||||
self.current_app = self.roku.current_app
|
||||
self._available = True
|
||||
except (RequestsConnectionError, RequestsReadTimeout, RokuException):
|
||||
self._available = False
|
||||
|
||||
def get_source_list(self):
|
||||
"""Get the list of applications to be used as sources."""
|
||||
return ["Home"] + sorted(channel.name for channel in self.roku.apps)
|
||||
self._unique_id = unique_id
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Device should be polled."""
|
||||
return True
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID for this entity."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
if self._device_info.user_device_name:
|
||||
return self._device_info.user_device_name
|
||||
|
||||
return f"Roku {self._device_info.serial_num}"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
def state(self) -> str:
|
||||
"""Return the state of the device."""
|
||||
if self._power_state == "Off":
|
||||
if self.coordinator.data.state.standby:
|
||||
return STATE_STANDBY
|
||||
|
||||
if self.current_app is None:
|
||||
if self.coordinator.data.app is None:
|
||||
return None
|
||||
|
||||
if self.current_app.name == "Power Saver" or self.current_app.is_screensaver:
|
||||
if (
|
||||
self.coordinator.data.app.name == "Power Saver"
|
||||
or self.coordinator.data.app.screensaver
|
||||
):
|
||||
return STATE_IDLE
|
||||
|
||||
if self.current_app.name == "Roku":
|
||||
if self.coordinator.data.app.name == "Roku":
|
||||
return STATE_HOME
|
||||
|
||||
if self.current_app.name is not None:
|
||||
if self.coordinator.data.app.name is not None:
|
||||
return STATE_PLAYING
|
||||
|
||||
return None
|
||||
|
@ -113,109 +90,108 @@ class RokuDevice(MediaPlayerEntity):
|
|||
return SUPPORT_ROKU
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if able to retrieve information from device or not."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique, Home Assistant friendly identifier for this entity."""
|
||||
return self._device_info.serial_num
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device specific attributes."""
|
||||
return {
|
||||
"name": self.name,
|
||||
"identifiers": {(DOMAIN, self.unique_id)},
|
||||
"manufacturer": DEFAULT_MANUFACTURER,
|
||||
"model": self._device_info.model_num,
|
||||
"sw_version": self._device_info.software_version,
|
||||
}
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
def media_content_type(self) -> str:
|
||||
"""Content type of current playing media."""
|
||||
if self.current_app is None or self.current_app.name in ("Power Saver", "Roku"):
|
||||
if self.app_id is None or self.app_name in ("Power Saver", "Roku"):
|
||||
return None
|
||||
|
||||
return MEDIA_TYPE_CHANNEL
|
||||
if self.app_id == "tvinput.dtv" and self.coordinator.data.channel is not None:
|
||||
return MEDIA_TYPE_CHANNEL
|
||||
|
||||
return MEDIA_TYPE_APP
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
def media_image_url(self) -> str:
|
||||
"""Image url of current playing media."""
|
||||
if self.current_app is None or self.current_app.name in ("Power Saver", "Roku"):
|
||||
if self.app_id is None or self.app_name in ("Power Saver", "Roku"):
|
||||
return None
|
||||
|
||||
if self.current_app.id is None:
|
||||
return None
|
||||
|
||||
return (
|
||||
f"http://{self.ip_address}:{DEFAULT_PORT}/query/icon/{self.current_app.id}"
|
||||
)
|
||||
return self.coordinator.roku.app_icon_url(self.app_id)
|
||||
|
||||
@property
|
||||
def app_name(self):
|
||||
def app_name(self) -> str:
|
||||
"""Name of the current running app."""
|
||||
if self.current_app is not None:
|
||||
return self.current_app.name
|
||||
if self.coordinator.data.app is not None:
|
||||
return self.coordinator.data.app.name
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def app_id(self):
|
||||
def app_id(self) -> str:
|
||||
"""Return the ID of the current running app."""
|
||||
if self.current_app is not None:
|
||||
return self.current_app.id
|
||||
if self.coordinator.data.app is not None:
|
||||
return self.coordinator.data.app.app_id
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
def media_channel(self):
|
||||
"""Return the TV channel currently tuned."""
|
||||
if self.app_id != "tvinput.dtv" or self.coordinator.data.channel is None:
|
||||
return None
|
||||
|
||||
if self.coordinator.data.channel.name is not None:
|
||||
return f"{self.coordinator.data.channel.name} ({self.coordinator.data.channel.number})"
|
||||
|
||||
return self.coordinator.data.channel.number
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Return the title of current playing media."""
|
||||
if self.app_id != "tvinput.dtv" or self.coordinator.data.channel is None:
|
||||
return None
|
||||
|
||||
if self.coordinator.data.channel.program_title is not None:
|
||||
return self.coordinator.data.channel.program_title
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def source(self) -> str:
|
||||
"""Return the current input source."""
|
||||
if self.current_app is not None:
|
||||
return self.current_app.name
|
||||
if self.coordinator.data.app is not None:
|
||||
return self.coordinator.data.app.name
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
def source_list(self) -> List:
|
||||
"""List of available input sources."""
|
||||
return self.channels
|
||||
return ["Home"] + sorted(app.name for app in self.coordinator.data.apps)
|
||||
|
||||
def turn_on(self):
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn on the Roku."""
|
||||
self.roku.poweron()
|
||||
await self.coordinator.roku.remote("poweron")
|
||||
|
||||
def turn_off(self):
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn off the Roku."""
|
||||
self.roku.poweroff()
|
||||
await self.coordinator.roku.remote("poweroff")
|
||||
|
||||
def media_play_pause(self):
|
||||
async def async_media_play_pause(self) -> None:
|
||||
"""Send play/pause command."""
|
||||
if self.current_app is not None:
|
||||
self.roku.play()
|
||||
await self.coordinator.roku.remote("play")
|
||||
|
||||
def media_previous_track(self):
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send previous track command."""
|
||||
if self.current_app is not None:
|
||||
self.roku.reverse()
|
||||
await self.coordinator.roku.remote("reverse")
|
||||
|
||||
def media_next_track(self):
|
||||
async def async_media_next_track(self) -> None:
|
||||
"""Send next track command."""
|
||||
if self.current_app is not None:
|
||||
self.roku.forward()
|
||||
await self.coordinator.roku.remote("forward")
|
||||
|
||||
def mute_volume(self, mute):
|
||||
async def async_mute_volume(self, mute) -> None:
|
||||
"""Mute the volume."""
|
||||
if self.current_app is not None:
|
||||
self.roku.volume_mute()
|
||||
await self.coordinator.roku.remote("volume_mute")
|
||||
|
||||
def volume_up(self):
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Volume up media player."""
|
||||
if self.current_app is not None:
|
||||
self.roku.volume_up()
|
||||
await self.coordinator.roku.remote("volume_up")
|
||||
|
||||
def volume_down(self):
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Volume down media player."""
|
||||
if self.current_app is not None:
|
||||
self.roku.volume_down()
|
||||
await self.coordinator.roku.remote("volume_down")
|
||||
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None:
|
||||
"""Tune to channel."""
|
||||
if media_type != MEDIA_TYPE_CHANNEL:
|
||||
_LOGGER.error(
|
||||
|
@ -225,16 +201,16 @@ class RokuDevice(MediaPlayerEntity):
|
|||
)
|
||||
return
|
||||
|
||||
if self.current_app is not None:
|
||||
self.roku.launch(self.roku["tvinput.dtv"], {"ch": media_id})
|
||||
await self.coordinator.roku.tune(media_id)
|
||||
|
||||
def select_source(self, source):
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select input source."""
|
||||
if self.current_app is None:
|
||||
return
|
||||
|
||||
if source == "Home":
|
||||
self.roku.home()
|
||||
else:
|
||||
channel = self.roku[source]
|
||||
channel.launch()
|
||||
await self.coordinator.roku.remote("home")
|
||||
|
||||
appl = next(
|
||||
(app for app in self.coordinator.data.apps if app.name == source), None
|
||||
)
|
||||
|
||||
if appl is not None:
|
||||
await self.coordinator.roku.launch(appl.app_id)
|
||||
|
|
|
@ -1,17 +1,12 @@
|
|||
"""Support for the Roku remote."""
|
||||
from typing import Callable, List
|
||||
|
||||
from requests.exceptions import (
|
||||
ConnectionError as RequestsConnectionError,
|
||||
ReadTimeout as RequestsReadTimeout,
|
||||
)
|
||||
from roku import RokuException
|
||||
|
||||
from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from .const import DATA_CLIENT, DEFAULT_MANUFACTURER, DOMAIN
|
||||
from . import RokuDataUpdateCoordinator, RokuEntity
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
@ -20,75 +15,38 @@ async def async_setup_entry(
|
|||
async_add_entities: Callable[[List, bool], None],
|
||||
) -> bool:
|
||||
"""Load Roku remote based on a config entry."""
|
||||
roku = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT]
|
||||
async_add_entities([RokuRemote(roku)], True)
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
unique_id = coordinator.data.info.serial_number
|
||||
async_add_entities([RokuRemote(unique_id, coordinator)], True)
|
||||
|
||||
|
||||
class RokuRemote(RemoteEntity):
|
||||
class RokuRemote(RokuEntity, RemoteEntity):
|
||||
"""Device that sends commands to an Roku."""
|
||||
|
||||
def __init__(self, roku):
|
||||
def __init__(self, unique_id: str, coordinator: RokuDataUpdateCoordinator) -> None:
|
||||
"""Initialize the Roku device."""
|
||||
self.roku = roku
|
||||
self._available = False
|
||||
self._device_info = {}
|
||||
super().__init__(
|
||||
device_id=unique_id,
|
||||
name=coordinator.data.info.name,
|
||||
coordinator=coordinator,
|
||||
)
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
try:
|
||||
self._device_info = self.roku.device_info
|
||||
self._available = True
|
||||
except (RequestsConnectionError, RequestsReadTimeout, RokuException):
|
||||
self._available = False
|
||||
self._unique_id = unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
if self._device_info.user_device_name:
|
||||
return self._device_info.user_device_name
|
||||
return f"Roku {self._device_info.serial_num}"
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID for this entity."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if able to retrieve information from device or not."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._device_info.serial_num
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device specific attributes."""
|
||||
return {
|
||||
"name": self.name,
|
||||
"identifiers": {(DOMAIN, self.unique_id)},
|
||||
"manufacturer": DEFAULT_MANUFACTURER,
|
||||
"model": self._device_info.model_num,
|
||||
"sw_version": self._device_info.software_version,
|
||||
}
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if device is on."""
|
||||
return True
|
||||
return not self.coordinator.data.state.standby
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed for Roku."""
|
||||
return False
|
||||
|
||||
def send_command(self, command, **kwargs):
|
||||
async def async_send_command(self, command: List, **kwargs) -> None:
|
||||
"""Send a command to one device."""
|
||||
num_repeats = kwargs[ATTR_NUM_REPEATS]
|
||||
|
||||
for _ in range(num_repeats):
|
||||
for single_command in command:
|
||||
if not hasattr(self.roku, single_command):
|
||||
continue
|
||||
|
||||
getattr(self.roku, single_command)()
|
||||
await self.coordinator.roku.remote(single_command)
|
||||
|
|
|
@ -1850,7 +1850,7 @@ rjpl==0.3.5
|
|||
rocketchat-API==0.6.1
|
||||
|
||||
# homeassistant.components.roku
|
||||
roku==4.1.0
|
||||
rokuecp==0.2.0
|
||||
|
||||
# homeassistant.components.roomba
|
||||
roombapy==1.5.3
|
||||
|
|
|
@ -735,7 +735,7 @@ rflink==0.0.52
|
|||
ring_doorbell==0.6.0
|
||||
|
||||
# homeassistant.components.roku
|
||||
roku==4.1.0
|
||||
rokuecp==0.2.0
|
||||
|
||||
# homeassistant.components.roomba
|
||||
roombapy==1.5.3
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
"""Tests for the Roku component."""
|
||||
from requests_mock import Mocker
|
||||
import re
|
||||
from socket import gaierror as SocketGIAError
|
||||
|
||||
from homeassistant.components.roku.const import DOMAIN
|
||||
from homeassistant.components.ssdp import (
|
||||
ATTR_SSDP_LOCATION,
|
||||
ATTR_UPNP_FRIENDLY_NAME,
|
||||
ATTR_UPNP_SERIAL,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
HOST = "192.168.1.160"
|
||||
NAME = "Roku 3"
|
||||
|
@ -13,38 +20,132 @@ SSDP_LOCATION = "http://192.168.1.160/"
|
|||
UPNP_FRIENDLY_NAME = "My Roku 3"
|
||||
UPNP_SERIAL = "1GU48T017973"
|
||||
|
||||
MOCK_SSDP_DISCOVERY_INFO = {
|
||||
ATTR_SSDP_LOCATION: SSDP_LOCATION,
|
||||
ATTR_UPNP_FRIENDLY_NAME: UPNP_FRIENDLY_NAME,
|
||||
ATTR_UPNP_SERIAL: UPNP_SERIAL,
|
||||
}
|
||||
|
||||
|
||||
def mock_connection(
|
||||
requests_mocker: Mocker, device: str = "roku3", app: str = "roku", host: str = HOST,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
device: str = "roku3",
|
||||
app: str = "roku",
|
||||
host: str = HOST,
|
||||
power: bool = True,
|
||||
error: bool = False,
|
||||
server_error: bool = False,
|
||||
) -> None:
|
||||
"""Mock the Roku connection."""
|
||||
roku_url = f"http://{host}:8060"
|
||||
|
||||
requests_mocker.get(
|
||||
if error:
|
||||
mock_connection_error(
|
||||
aioclient_mock=aioclient_mock, device=device, app=app, host=host
|
||||
)
|
||||
return
|
||||
|
||||
if server_error:
|
||||
mock_connection_server_error(
|
||||
aioclient_mock=aioclient_mock, device=device, app=app, host=host
|
||||
)
|
||||
return
|
||||
|
||||
info_fixture = f"roku/{device}-device-info.xml"
|
||||
if not power:
|
||||
info_fixture = f"roku/{device}-device-info-power-off.xml"
|
||||
|
||||
aioclient_mock.get(
|
||||
f"{roku_url}/query/device-info",
|
||||
text=load_fixture(f"roku/{device}-device-info.xml"),
|
||||
text=load_fixture(info_fixture),
|
||||
headers={"Content-Type": "application/xml"},
|
||||
)
|
||||
|
||||
apps_fixture = "roku/apps.xml"
|
||||
if device == "rokutv":
|
||||
apps_fixture = "roku/apps-tv.xml"
|
||||
|
||||
requests_mocker.get(
|
||||
f"{roku_url}/query/apps", text=load_fixture(apps_fixture),
|
||||
aioclient_mock.get(
|
||||
f"{roku_url}/query/apps",
|
||||
text=load_fixture(apps_fixture),
|
||||
headers={"Content-Type": "application/xml"},
|
||||
)
|
||||
|
||||
requests_mocker.get(
|
||||
f"{roku_url}/query/active-app", text=load_fixture(f"roku/active-app-{app}.xml"),
|
||||
aioclient_mock.get(
|
||||
f"{roku_url}/query/active-app",
|
||||
text=load_fixture(f"roku/active-app-{app}.xml"),
|
||||
headers={"Content-Type": "application/xml"},
|
||||
)
|
||||
|
||||
aioclient_mock.get(
|
||||
f"{roku_url}/query/tv-active-channel",
|
||||
text=load_fixture("roku/rokutv-tv-active-channel.xml"),
|
||||
headers={"Content-Type": "application/xml"},
|
||||
)
|
||||
|
||||
aioclient_mock.get(
|
||||
f"{roku_url}/query/tv-channels",
|
||||
text=load_fixture("roku/rokutv-tv-channels.xml"),
|
||||
headers={"Content-Type": "application/xml"},
|
||||
)
|
||||
|
||||
aioclient_mock.post(
|
||||
re.compile(f"{roku_url}/keypress/.*"), text="OK",
|
||||
)
|
||||
|
||||
aioclient_mock.post(
|
||||
re.compile(f"{roku_url}/launch/.*"), text="OK",
|
||||
)
|
||||
|
||||
|
||||
def mock_connection_error(
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
device: str = "roku3",
|
||||
app: str = "roku",
|
||||
host: str = HOST,
|
||||
) -> None:
|
||||
"""Mock the Roku connection error."""
|
||||
roku_url = f"http://{host}:8060"
|
||||
|
||||
aioclient_mock.get(f"{roku_url}/query/device-info", exc=SocketGIAError)
|
||||
aioclient_mock.get(f"{roku_url}/query/apps", exc=SocketGIAError)
|
||||
aioclient_mock.get(f"{roku_url}/query/active-app", exc=SocketGIAError)
|
||||
aioclient_mock.get(f"{roku_url}/query/tv-active-channel", exc=SocketGIAError)
|
||||
aioclient_mock.get(f"{roku_url}/query/tv-channels", exc=SocketGIAError)
|
||||
|
||||
aioclient_mock.post(re.compile(f"{roku_url}/keypress/.*"), exc=SocketGIAError)
|
||||
aioclient_mock.post(re.compile(f"{roku_url}/launch/.*"), exc=SocketGIAError)
|
||||
|
||||
|
||||
def mock_connection_server_error(
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
device: str = "roku3",
|
||||
app: str = "roku",
|
||||
host: str = HOST,
|
||||
) -> None:
|
||||
"""Mock the Roku server error."""
|
||||
roku_url = f"http://{host}:8060"
|
||||
|
||||
aioclient_mock.get(f"{roku_url}/query/device-info", status=500)
|
||||
aioclient_mock.get(f"{roku_url}/query/apps", status=500)
|
||||
aioclient_mock.get(f"{roku_url}/query/active-app", status=500)
|
||||
aioclient_mock.get(f"{roku_url}/query/tv-active-channel", status=500)
|
||||
aioclient_mock.get(f"{roku_url}/query/tv-channels", status=500)
|
||||
|
||||
aioclient_mock.post(re.compile(f"{roku_url}/keypress/.*"), status=500)
|
||||
aioclient_mock.post(re.compile(f"{roku_url}/launch/.*"), status=500)
|
||||
|
||||
|
||||
async def setup_integration(
|
||||
hass: HomeAssistantType,
|
||||
requests_mocker: Mocker,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
device: str = "roku3",
|
||||
app: str = "roku",
|
||||
host: str = HOST,
|
||||
unique_id: str = UPNP_SERIAL,
|
||||
error: bool = False,
|
||||
power: bool = True,
|
||||
server_error: bool = False,
|
||||
skip_entry_setup: bool = False,
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the Roku integration in Home Assistant."""
|
||||
|
@ -53,7 +154,15 @@ async def setup_integration(
|
|||
entry.add_to_hass(hass)
|
||||
|
||||
if not skip_entry_setup:
|
||||
mock_connection(requests_mocker, device, app=app, host=host)
|
||||
mock_connection(
|
||||
aioclient_mock,
|
||||
device,
|
||||
app=app,
|
||||
host=host,
|
||||
error=error,
|
||||
power=power,
|
||||
server_error=server_error,
|
||||
)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
|
|
@ -1,16 +1,5 @@
|
|||
"""Test the Roku config flow."""
|
||||
from socket import gaierror as SocketGIAError
|
||||
|
||||
from requests.exceptions import RequestException
|
||||
from requests_mock import Mocker
|
||||
from roku import RokuException
|
||||
|
||||
from homeassistant.components.roku.const import DOMAIN
|
||||
from homeassistant.components.ssdp import (
|
||||
ATTR_SSDP_LOCATION,
|
||||
ATTR_UPNP_FRIENDLY_NAME,
|
||||
ATTR_UPNP_SERIAL,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE
|
||||
from homeassistant.data_entry_flow import (
|
||||
|
@ -24,19 +13,20 @@ from homeassistant.setup import async_setup_component
|
|||
from tests.async_mock import patch
|
||||
from tests.components.roku import (
|
||||
HOST,
|
||||
SSDP_LOCATION,
|
||||
MOCK_SSDP_DISCOVERY_INFO,
|
||||
UPNP_FRIENDLY_NAME,
|
||||
UPNP_SERIAL,
|
||||
mock_connection,
|
||||
setup_integration,
|
||||
)
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
async def test_duplicate_error(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
||||
async def test_duplicate_error(
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test that errors are shown when duplicates are added."""
|
||||
await setup_integration(hass, requests_mock, skip_entry_setup=True)
|
||||
|
||||
mock_connection(requests_mock)
|
||||
await setup_integration(hass, aioclient_mock, skip_entry_setup=True)
|
||||
mock_connection(aioclient_mock)
|
||||
|
||||
user_input = {CONF_HOST: HOST}
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
@ -54,11 +44,7 @@ async def test_duplicate_error(hass: HomeAssistantType, requests_mock: Mocker) -
|
|||
assert result["type"] == RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
discovery_info = {
|
||||
ATTR_UPNP_FRIENDLY_NAME: UPNP_FRIENDLY_NAME,
|
||||
ATTR_SSDP_LOCATION: SSDP_LOCATION,
|
||||
ATTR_UPNP_SERIAL: UPNP_SERIAL,
|
||||
}
|
||||
discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info
|
||||
)
|
||||
|
@ -67,11 +53,12 @@ async def test_duplicate_error(hass: HomeAssistantType, requests_mock: Mocker) -
|
|||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
||||
async def test_form(
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the user step."""
|
||||
await async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
mock_connection(requests_mock)
|
||||
mock_connection(aioclient_mock)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
|
||||
|
@ -90,7 +77,7 @@ async def test_form(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
|||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == HOST
|
||||
assert result["title"] == UPNP_FRIENDLY_NAME
|
||||
|
||||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == HOST
|
||||
|
@ -100,70 +87,23 @@ async def test_form(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
|||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass: HomeAssistantType) -> None:
|
||||
async def test_form_cannot_connect(
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we handle cannot connect roku error."""
|
||||
mock_connection(aioclient_mock, error=True)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.roku.config_flow.Roku._call",
|
||||
side_effect=RokuException,
|
||||
) as mock_validate_input:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id=result["flow_id"], user_input={CONF_HOST: HOST}
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_validate_input.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_cannot_connect_request(hass: HomeAssistantType) -> None:
|
||||
"""Test we handle cannot connect request error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id=result["flow_id"], user_input={CONF_HOST: HOST}
|
||||
)
|
||||
|
||||
user_input = {CONF_HOST: HOST}
|
||||
with patch(
|
||||
"homeassistant.components.roku.config_flow.Roku._call",
|
||||
side_effect=RequestException,
|
||||
) as mock_validate_input:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id=result["flow_id"], user_input=user_input
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_validate_input.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_cannot_connect_socket(hass: HomeAssistantType) -> None:
|
||||
"""Test we handle cannot connect socket error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
|
||||
)
|
||||
|
||||
user_input = {CONF_HOST: HOST}
|
||||
with patch(
|
||||
"homeassistant.components.roku.config_flow.Roku._call",
|
||||
side_effect=SocketGIAError,
|
||||
) as mock_validate_input:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id=result["flow_id"], user_input=user_input
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_validate_input.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_unknown_error(hass: HomeAssistantType) -> None:
|
||||
"""Test we handle unknown error."""
|
||||
|
@ -173,7 +113,7 @@ async def test_form_unknown_error(hass: HomeAssistantType) -> None:
|
|||
|
||||
user_input = {CONF_HOST: HOST}
|
||||
with patch(
|
||||
"homeassistant.components.roku.config_flow.Roku._call", side_effect=Exception,
|
||||
"homeassistant.components.roku.config_flow.Roku.update", side_effect=Exception,
|
||||
) as mock_validate_input:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow_id=result["flow_id"], user_input=user_input
|
||||
|
@ -186,9 +126,11 @@ async def test_form_unknown_error(hass: HomeAssistantType) -> None:
|
|||
assert len(mock_validate_input.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_import(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
||||
async def test_import(
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the import step."""
|
||||
mock_connection(requests_mock)
|
||||
mock_connection(aioclient_mock)
|
||||
|
||||
user_input = {CONF_HOST: HOST}
|
||||
with patch(
|
||||
|
@ -201,7 +143,7 @@ async def test_import(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
|||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == HOST
|
||||
assert result["title"] == UPNP_FRIENDLY_NAME
|
||||
|
||||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == HOST
|
||||
|
@ -211,15 +153,44 @@ async def test_import(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
|||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_ssdp_discovery(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
||||
"""Test the ssdp discovery step."""
|
||||
mock_connection(requests_mock)
|
||||
async def test_ssdp_cannot_connect(
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort SSDP flow on connection error."""
|
||||
mock_connection(aioclient_mock, error=True)
|
||||
|
||||
discovery_info = {
|
||||
ATTR_SSDP_LOCATION: SSDP_LOCATION,
|
||||
ATTR_UPNP_FRIENDLY_NAME: UPNP_FRIENDLY_NAME,
|
||||
ATTR_UPNP_SERIAL: UPNP_SERIAL,
|
||||
}
|
||||
discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_ssdp_unknown_error(
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort SSDP flow on unknown error."""
|
||||
discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy()
|
||||
with patch(
|
||||
"homeassistant.components.roku.config_flow.Roku.update", side_effect=Exception,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "unknown"
|
||||
|
||||
|
||||
async def test_ssdp_discovery(
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the SSDP discovery flow."""
|
||||
mock_connection(aioclient_mock)
|
||||
|
||||
discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info
|
||||
)
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
"""Tests for the Roku integration."""
|
||||
from socket import gaierror as SocketGIAError
|
||||
|
||||
from requests.exceptions import RequestException
|
||||
from requests_mock import Mocker
|
||||
from roku import RokuException
|
||||
|
||||
from homeassistant.components.roku.const import DOMAIN
|
||||
from homeassistant.config_entries import (
|
||||
ENTRY_STATE_LOADED,
|
||||
|
@ -15,46 +9,20 @@ from homeassistant.helpers.typing import HomeAssistantType
|
|||
|
||||
from tests.async_mock import patch
|
||||
from tests.components.roku import setup_integration
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
async def test_config_entry_not_ready(
|
||||
hass: HomeAssistantType, requests_mock: Mocker
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the Roku configuration entry not ready."""
|
||||
with patch(
|
||||
"homeassistant.components.roku.Roku._call", side_effect=RokuException,
|
||||
):
|
||||
entry = await setup_integration(hass, requests_mock)
|
||||
|
||||
assert entry.state == ENTRY_STATE_SETUP_RETRY
|
||||
|
||||
|
||||
async def test_config_entry_not_ready_request(
|
||||
hass: HomeAssistantType, requests_mock: Mocker
|
||||
) -> None:
|
||||
"""Test the Roku configuration entry not ready."""
|
||||
with patch(
|
||||
"homeassistant.components.roku.Roku._call", side_effect=RequestException,
|
||||
):
|
||||
entry = await setup_integration(hass, requests_mock)
|
||||
|
||||
assert entry.state == ENTRY_STATE_SETUP_RETRY
|
||||
|
||||
|
||||
async def test_config_entry_not_ready_socket(
|
||||
hass: HomeAssistantType, requests_mock: Mocker
|
||||
) -> None:
|
||||
"""Test the Roku configuration entry not ready."""
|
||||
with patch(
|
||||
"homeassistant.components.roku.Roku._call", side_effect=SocketGIAError,
|
||||
):
|
||||
entry = await setup_integration(hass, requests_mock)
|
||||
entry = await setup_integration(hass, aioclient_mock, error=True)
|
||||
|
||||
assert entry.state == ENTRY_STATE_SETUP_RETRY
|
||||
|
||||
|
||||
async def test_unload_config_entry(
|
||||
hass: HomeAssistantType, requests_mock: Mocker
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the Roku configuration entry unloading."""
|
||||
with patch(
|
||||
|
@ -63,7 +31,7 @@ async def test_unload_config_entry(
|
|||
), patch(
|
||||
"homeassistant.components.roku.remote.async_setup_entry", return_value=True,
|
||||
):
|
||||
entry = await setup_integration(hass, requests_mock)
|
||||
entry = await setup_integration(hass, aioclient_mock)
|
||||
|
||||
assert hass.data[DOMAIN][entry.entry_id]
|
||||
assert entry.state == ENTRY_STATE_LOADED
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
"""Tests for the Roku Media Player platform."""
|
||||
from datetime import timedelta
|
||||
|
||||
from requests.exceptions import (
|
||||
ConnectionError as RequestsConnectionError,
|
||||
ReadTimeout as RequestsReadTimeout,
|
||||
)
|
||||
from requests_mock import Mocker
|
||||
from roku import RokuException
|
||||
from rokuecp import RokuError
|
||||
|
||||
from homeassistant.components.media_player.const import (
|
||||
ATTR_APP_ID,
|
||||
ATTR_APP_NAME,
|
||||
ATTR_INPUT_SOURCE,
|
||||
ATTR_MEDIA_CHANNEL,
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE,
|
||||
ATTR_MEDIA_TITLE,
|
||||
ATTR_MEDIA_VOLUME_MUTED,
|
||||
DOMAIN as MP_DOMAIN,
|
||||
MEDIA_TYPE_APP,
|
||||
MEDIA_TYPE_CHANNEL,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
|
@ -38,6 +38,7 @@ from homeassistant.const import (
|
|||
SERVICE_VOLUME_MUTE,
|
||||
SERVICE_VOLUME_UP,
|
||||
STATE_HOME,
|
||||
STATE_IDLE,
|
||||
STATE_PLAYING,
|
||||
STATE_STANDBY,
|
||||
STATE_UNAVAILABLE,
|
||||
|
@ -45,9 +46,10 @@ from homeassistant.const import (
|
|||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.async_mock import PropertyMock, patch
|
||||
from tests.async_mock import patch
|
||||
from tests.common import async_fire_time_changed
|
||||
from tests.components.roku import UPNP_SERIAL, setup_integration
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
MAIN_ENTITY_ID = f"{MP_DOMAIN}.my_roku_3"
|
||||
TV_ENTITY_ID = f"{MP_DOMAIN}.58_onn_roku_tv"
|
||||
|
@ -56,34 +58,37 @@ TV_HOST = "192.168.1.161"
|
|||
TV_SERIAL = "YN00H5555555"
|
||||
|
||||
|
||||
async def test_setup(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
||||
async def test_setup(
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test setup with basic config."""
|
||||
await setup_integration(hass, requests_mock)
|
||||
await setup_integration(hass, aioclient_mock)
|
||||
|
||||
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
||||
|
||||
main = entity_registry.async_get(MAIN_ENTITY_ID)
|
||||
|
||||
assert hass.states.get(MAIN_ENTITY_ID)
|
||||
assert main
|
||||
assert main.unique_id == UPNP_SERIAL
|
||||
|
||||
|
||||
async def test_idle_setup(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
||||
async def test_idle_setup(
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test setup with idle device."""
|
||||
with patch(
|
||||
"homeassistant.components.roku.Roku.power_state",
|
||||
new_callable=PropertyMock(return_value="Off"),
|
||||
):
|
||||
await setup_integration(hass, requests_mock)
|
||||
await setup_integration(hass, aioclient_mock, power=False)
|
||||
|
||||
state = hass.states.get(MAIN_ENTITY_ID)
|
||||
assert state.state == STATE_STANDBY
|
||||
|
||||
|
||||
async def test_tv_setup(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
||||
async def test_tv_setup(
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test Roku TV setup."""
|
||||
await setup_integration(
|
||||
hass,
|
||||
requests_mock,
|
||||
aioclient_mock,
|
||||
device="rokutv",
|
||||
app="tvinput-dtv",
|
||||
host=TV_HOST,
|
||||
|
@ -91,41 +96,26 @@ async def test_tv_setup(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
|||
)
|
||||
|
||||
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
||||
|
||||
tv = entity_registry.async_get(TV_ENTITY_ID)
|
||||
|
||||
assert hass.states.get(TV_ENTITY_ID)
|
||||
assert tv
|
||||
assert tv.unique_id == TV_SERIAL
|
||||
|
||||
|
||||
async def test_availability(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
||||
async def test_availability(
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test entity availability."""
|
||||
now = dt_util.utcnow()
|
||||
future = now + timedelta(minutes=1)
|
||||
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=now):
|
||||
await setup_integration(hass, requests_mock)
|
||||
await setup_integration(hass, aioclient_mock)
|
||||
|
||||
with patch("roku.Roku._get", side_effect=RokuException,), patch(
|
||||
"homeassistant.util.dt.utcnow", return_value=future
|
||||
):
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(MAIN_ENTITY_ID).state == STATE_UNAVAILABLE
|
||||
|
||||
future += timedelta(minutes=1)
|
||||
|
||||
with patch("roku.Roku._get", side_effect=RequestsConnectionError,), patch(
|
||||
"homeassistant.util.dt.utcnow", return_value=future
|
||||
):
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(MAIN_ENTITY_ID).state == STATE_UNAVAILABLE
|
||||
|
||||
future += timedelta(minutes=1)
|
||||
|
||||
with patch("roku.Roku._get", side_effect=RequestsReadTimeout,), patch(
|
||||
"homeassistant.util.dt.utcnow", return_value=future
|
||||
):
|
||||
with patch(
|
||||
"homeassistant.components.roku.Roku.update", side_effect=RokuError
|
||||
), patch("homeassistant.util.dt.utcnow", return_value=future):
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(MAIN_ENTITY_ID).state == STATE_UNAVAILABLE
|
||||
|
@ -139,10 +129,10 @@ async def test_availability(hass: HomeAssistantType, requests_mock: Mocker) -> N
|
|||
|
||||
|
||||
async def test_supported_features(
|
||||
hass: HomeAssistantType, requests_mock: Mocker
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test supported features."""
|
||||
await setup_integration(hass, requests_mock)
|
||||
await setup_integration(hass, aioclient_mock)
|
||||
|
||||
# Features supported for Rokus
|
||||
state = hass.states.get(MAIN_ENTITY_ID)
|
||||
|
@ -161,12 +151,12 @@ async def test_supported_features(
|
|||
|
||||
|
||||
async def test_tv_supported_features(
|
||||
hass: HomeAssistantType, requests_mock: Mocker
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test supported features for Roku TV."""
|
||||
await setup_integration(
|
||||
hass,
|
||||
requests_mock,
|
||||
aioclient_mock,
|
||||
device="rokutv",
|
||||
app="tvinput-dtv",
|
||||
host=TV_HOST,
|
||||
|
@ -188,22 +178,58 @@ async def test_tv_supported_features(
|
|||
)
|
||||
|
||||
|
||||
async def test_attributes(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
||||
async def test_attributes(
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test attributes."""
|
||||
await setup_integration(hass, requests_mock)
|
||||
await setup_integration(hass, aioclient_mock)
|
||||
|
||||
state = hass.states.get(MAIN_ENTITY_ID)
|
||||
assert state.state == STATE_HOME
|
||||
|
||||
assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) is None
|
||||
assert state.attributes.get(ATTR_APP_ID) is None
|
||||
assert state.attributes.get(ATTR_APP_NAME) == "Roku"
|
||||
assert state.attributes.get(ATTR_INPUT_SOURCE) == "Roku"
|
||||
|
||||
|
||||
async def test_tv_attributes(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
||||
async def test_attributes_app(
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test attributes for app."""
|
||||
await setup_integration(hass, aioclient_mock, app="netflix")
|
||||
|
||||
state = hass.states.get(MAIN_ENTITY_ID)
|
||||
assert state.state == STATE_PLAYING
|
||||
|
||||
assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_APP
|
||||
assert state.attributes.get(ATTR_APP_ID) == "12"
|
||||
assert state.attributes.get(ATTR_APP_NAME) == "Netflix"
|
||||
assert state.attributes.get(ATTR_INPUT_SOURCE) == "Netflix"
|
||||
|
||||
|
||||
async def test_attributes_screensaver(
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test attributes for app with screensaver."""
|
||||
await setup_integration(hass, aioclient_mock, app="screensaver")
|
||||
|
||||
state = hass.states.get(MAIN_ENTITY_ID)
|
||||
assert state.state == STATE_IDLE
|
||||
|
||||
assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) is None
|
||||
assert state.attributes.get(ATTR_APP_ID) is None
|
||||
assert state.attributes.get(ATTR_APP_NAME) == "Roku"
|
||||
assert state.attributes.get(ATTR_INPUT_SOURCE) == "Roku"
|
||||
|
||||
|
||||
async def test_tv_attributes(
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test attributes for Roku TV."""
|
||||
await setup_integration(
|
||||
hass,
|
||||
requests_mock,
|
||||
aioclient_mock,
|
||||
device="rokutv",
|
||||
app="tvinput-dtv",
|
||||
host=TV_HOST,
|
||||
|
@ -213,29 +239,35 @@ async def test_tv_attributes(hass: HomeAssistantType, requests_mock: Mocker) ->
|
|||
state = hass.states.get(TV_ENTITY_ID)
|
||||
assert state.state == STATE_PLAYING
|
||||
|
||||
assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_CHANNEL
|
||||
assert state.attributes.get(ATTR_APP_ID) == "tvinput.dtv"
|
||||
assert state.attributes.get(ATTR_APP_NAME) == "Antenna TV"
|
||||
assert state.attributes.get(ATTR_INPUT_SOURCE) == "Antenna TV"
|
||||
assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_CHANNEL
|
||||
assert state.attributes.get(ATTR_MEDIA_CHANNEL) == "getTV (14.3)"
|
||||
assert state.attributes.get(ATTR_MEDIA_TITLE) == "Airwolf"
|
||||
|
||||
|
||||
async def test_services(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
||||
async def test_services(
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the different media player services."""
|
||||
await setup_integration(hass, requests_mock)
|
||||
await setup_integration(hass, aioclient_mock)
|
||||
|
||||
with patch("roku.Roku._post") as remote_mock:
|
||||
with patch("homeassistant.components.roku.Roku.remote") as remote_mock:
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: MAIN_ENTITY_ID}, blocking=True
|
||||
)
|
||||
|
||||
remote_mock.assert_called_once_with("/keypress/PowerOff")
|
||||
remote_mock.assert_called_once_with("poweroff")
|
||||
|
||||
with patch("roku.Roku._post") as remote_mock:
|
||||
with patch("homeassistant.components.roku.Roku.remote") as remote_mock:
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: MAIN_ENTITY_ID}, blocking=True
|
||||
)
|
||||
|
||||
remote_mock.assert_called_once_with("/keypress/PowerOn")
|
||||
remote_mock.assert_called_once_with("poweron")
|
||||
|
||||
with patch("roku.Roku._post") as remote_mock:
|
||||
with patch("homeassistant.components.roku.Roku.remote") as remote_mock:
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_MEDIA_PLAY_PAUSE,
|
||||
|
@ -243,9 +275,9 @@ async def test_services(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
|||
blocking=True,
|
||||
)
|
||||
|
||||
remote_mock.assert_called_once_with("/keypress/Play")
|
||||
remote_mock.assert_called_once_with("play")
|
||||
|
||||
with patch("roku.Roku._post") as remote_mock:
|
||||
with patch("homeassistant.components.roku.Roku.remote") as remote_mock:
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_MEDIA_NEXT_TRACK,
|
||||
|
@ -253,9 +285,9 @@ async def test_services(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
|||
blocking=True,
|
||||
)
|
||||
|
||||
remote_mock.assert_called_once_with("/keypress/Fwd")
|
||||
remote_mock.assert_called_once_with("forward")
|
||||
|
||||
with patch("roku.Roku._post") as remote_mock:
|
||||
with patch("homeassistant.components.roku.Roku.remote") as remote_mock:
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK,
|
||||
|
@ -263,9 +295,9 @@ async def test_services(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
|||
blocking=True,
|
||||
)
|
||||
|
||||
remote_mock.assert_called_once_with("/keypress/Rev")
|
||||
remote_mock.assert_called_once_with("reverse")
|
||||
|
||||
with patch("roku.Roku._post") as remote_mock:
|
||||
with patch("homeassistant.components.roku.Roku.remote") as remote_mock:
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
|
@ -273,9 +305,9 @@ async def test_services(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
|||
blocking=True,
|
||||
)
|
||||
|
||||
remote_mock.assert_called_once_with("/keypress/Home")
|
||||
remote_mock.assert_called_once_with("home")
|
||||
|
||||
with patch("roku.Roku._post") as remote_mock:
|
||||
with patch("homeassistant.components.roku.Roku.launch") as remote_mock:
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
|
@ -283,28 +315,30 @@ async def test_services(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
|||
blocking=True,
|
||||
)
|
||||
|
||||
remote_mock.assert_called_once_with("/launch/12", params={"contentID": "12"})
|
||||
remote_mock.assert_called_once_with("12")
|
||||
|
||||
|
||||
async def test_tv_services(hass: HomeAssistantType, requests_mock: Mocker) -> None:
|
||||
async def test_tv_services(
|
||||
hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the media player services related to Roku TV."""
|
||||
await setup_integration(
|
||||
hass,
|
||||
requests_mock,
|
||||
aioclient_mock,
|
||||
device="rokutv",
|
||||
app="tvinput-dtv",
|
||||
host=TV_HOST,
|
||||
unique_id=TV_SERIAL,
|
||||
)
|
||||
|
||||
with patch("roku.Roku._post") as remote_mock:
|
||||
with patch("homeassistant.components.roku.Roku.remote") as remote_mock:
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: TV_ENTITY_ID}, blocking=True
|
||||
)
|
||||
|
||||
remote_mock.assert_called_once_with("/keypress/VolumeUp")
|
||||
remote_mock.assert_called_once_with("volume_up")
|
||||
|
||||
with patch("roku.Roku._post") as remote_mock:
|
||||
with patch("homeassistant.components.roku.Roku.remote") as remote_mock:
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_VOLUME_DOWN,
|
||||
|
@ -312,9 +346,9 @@ async def test_tv_services(hass: HomeAssistantType, requests_mock: Mocker) -> No
|
|||
blocking=True,
|
||||
)
|
||||
|
||||
remote_mock.assert_called_once_with("/keypress/VolumeDown")
|
||||
remote_mock.assert_called_once_with("volume_down")
|
||||
|
||||
with patch("roku.Roku._post") as remote_mock:
|
||||
with patch("homeassistant.components.roku.Roku.remote") as remote_mock:
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_VOLUME_MUTE,
|
||||
|
@ -322,9 +356,9 @@ async def test_tv_services(hass: HomeAssistantType, requests_mock: Mocker) -> No
|
|||
blocking=True,
|
||||
)
|
||||
|
||||
remote_mock.assert_called_once_with("/keypress/VolumeMute")
|
||||
remote_mock.assert_called_once_with("volume_mute")
|
||||
|
||||
with patch("roku.Roku.launch") as tune_mock:
|
||||
with patch("homeassistant.components.roku.Roku.tune") as tune_mock:
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
|
@ -336,4 +370,4 @@ async def test_tv_services(hass: HomeAssistantType, requests_mock: Mocker) -> No
|
|||
blocking=True,
|
||||
)
|
||||
|
||||
tune_mock.assert_called_once()
|
||||
tune_mock.assert_called_once_with("55")
|
||||
|
|
35
tests/fixtures/roku/roku3-device-info-power-off.xml
vendored
Normal file
35
tests/fixtures/roku/roku3-device-info-power-off.xml
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<device-info>
|
||||
<udn>015e5108-9000-1046-8035-b0a737964dfb</udn>
|
||||
<serial-number>1GU48T017973</serial-number>
|
||||
<device-id>1GU48T017973</device-id>
|
||||
<vendor-name>Roku</vendor-name>
|
||||
<model-number>4200X</model-number>
|
||||
<model-name>Roku 3</model-name>
|
||||
<model-region>US</model-region>
|
||||
<supports-ethernet>true</supports-ethernet>
|
||||
<wifi-mac>b0:a7:37:96:4d:fb</wifi-mac>
|
||||
<ethernet-mac>b0:a7:37:96:4d:fa</ethernet-mac>
|
||||
<network-type>ethernet</network-type>
|
||||
<user-device-name>My Roku 3</user-device-name>
|
||||
<software-version>7.5.0</software-version>
|
||||
<software-build>09021</software-build>
|
||||
<secure-device>true</secure-device>
|
||||
<language>en</language>
|
||||
<country>US</country>
|
||||
<locale>en_US</locale>
|
||||
<time-zone>US/Pacific</time-zone>
|
||||
<time-zone-offset>-480</time-zone-offset>
|
||||
<power-mode>PowerOff</power-mode>
|
||||
<supports-suspend>false</supports-suspend>
|
||||
<supports-find-remote>false</supports-find-remote>
|
||||
<supports-audio-guide>false</supports-audio-guide>
|
||||
<developer-enabled>true</developer-enabled>
|
||||
<keyed-developer-id>70f6ed9c90cf60718a26f3a7c3e5af1c3ec29558</keyed-developer-id>
|
||||
<search-enabled>true</search-enabled>
|
||||
<voice-search-enabled>true</voice-search-enabled>
|
||||
<notifications-enabled>true</notifications-enabled>
|
||||
<notifications-first-use>false</notifications-first-use>
|
||||
<supports-private-listening>false</supports-private-listening>
|
||||
<headphones-connected>false</headphones-connected>
|
||||
</device-info>
|
72
tests/fixtures/roku/rokutv-device-info-power-off.xml
vendored
Normal file
72
tests/fixtures/roku/rokutv-device-info-power-off.xml
vendored
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<device-info>
|
||||
<udn>015e5555-9000-5555-5555-b0a555555dfb</udn>
|
||||
<serial-number>YN00H5555555</serial-number>
|
||||
<device-id>0S596H055555</device-id>
|
||||
<advertising-id>055555a9-d82b-5c75-b8fe-5555550cb7ee</advertising-id>
|
||||
<vendor-name>Onn</vendor-name>
|
||||
<model-name>100005844</model-name>
|
||||
<model-number>7820X</model-number>
|
||||
<model-region>US</model-region>
|
||||
<is-tv>true</is-tv>
|
||||
<is-stick>false</is-stick>
|
||||
<screen-size>58</screen-size>
|
||||
<panel-id>2</panel-id>
|
||||
<tuner-type>ATSC</tuner-type>
|
||||
<supports-ethernet>true</supports-ethernet>
|
||||
<wifi-mac>d8:13:99:f8:b0:c6</wifi-mac>
|
||||
<wifi-driver>realtek</wifi-driver>
|
||||
<ethernet-mac>d4:3a:2e:07:fd:cb</ethernet-mac>
|
||||
<network-type>wifi</network-type>
|
||||
<network-name>NetworkSSID</network-name>
|
||||
<friendly-device-name>58" Onn Roku TV</friendly-device-name>
|
||||
<friendly-model-name>Onn Roku TV</friendly-model-name>
|
||||
<default-device-name>Onn Roku TV - YN00H5555555</default-device-name>
|
||||
<user-device-name>58" Onn Roku TV</user-device-name>
|
||||
<user-device-location>Living room</user-device-location>
|
||||
<build-number>AT9.20E04502A</build-number>
|
||||
<software-version>9.2.0</software-version>
|
||||
<software-build>4502</software-build>
|
||||
<secure-device>true</secure-device>
|
||||
<language>en</language>
|
||||
<country>US</country>
|
||||
<locale>en_US</locale>
|
||||
<time-zone-auto>true</time-zone-auto>
|
||||
<time-zone>US/Central</time-zone>
|
||||
<time-zone-name>United States/Central</time-zone-name>
|
||||
<time-zone-tz>America/Chicago</time-zone-tz>
|
||||
<time-zone-offset>-300</time-zone-offset>
|
||||
<clock-format>12-hour</clock-format>
|
||||
<uptime>264789</uptime>
|
||||
<power-mode>PowerOn</power-mode>
|
||||
<supports-suspend>true</supports-suspend>
|
||||
<supports-find-remote>true</supports-find-remote>
|
||||
<find-remote-is-possible>false</find-remote-is-possible>
|
||||
<supports-audio-guide>true</supports-audio-guide>
|
||||
<supports-rva>true</supports-rva>
|
||||
<developer-enabled>false</developer-enabled>
|
||||
<keyed-developer-id/>
|
||||
<search-enabled>true</search-enabled>
|
||||
<search-channels-enabled>true</search-channels-enabled>
|
||||
<voice-search-enabled>true</voice-search-enabled>
|
||||
<notifications-enabled>true</notifications-enabled>
|
||||
<notifications-first-use>false</notifications-first-use>
|
||||
<supports-private-listening>true</supports-private-listening>
|
||||
<supports-private-listening-dtv>true</supports-private-listening-dtv>
|
||||
<supports-warm-standby>true</supports-warm-standby>
|
||||
<headphones-connected>false</headphones-connected>
|
||||
<expert-pq-enabled>0.9</expert-pq-enabled>
|
||||
<supports-ecs-textedit>true</supports-ecs-textedit>
|
||||
<supports-ecs-microphone>true</supports-ecs-microphone>
|
||||
<supports-wake-on-wlan>true</supports-wake-on-wlan>
|
||||
<has-play-on-roku>true</has-play-on-roku>
|
||||
<has-mobile-screensaver>true</has-mobile-screensaver>
|
||||
<support-url>https://www.onntvsupport.com/</support-url>
|
||||
<grandcentral-version>2.9.57</grandcentral-version>
|
||||
<trc-version>3.0</trc-version>
|
||||
<trc-channel-version>2.9.42</trc-channel-version>
|
||||
<davinci-version>2.8.20</davinci-version>
|
||||
<has-wifi-extender>false</has-wifi-extender>
|
||||
<has-wifi-5G-support>true</has-wifi-5G-support>
|
||||
<can-use-wifi-extender>true</can-use-wifi-extender>
|
||||
</device-info>
|
24
tests/fixtures/roku/rokutv-tv-active-channel.xml
vendored
Normal file
24
tests/fixtures/roku/rokutv-tv-active-channel.xml
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<tv-channel>
|
||||
<channel>
|
||||
<number>14.3</number>
|
||||
<name>getTV</name>
|
||||
<type>air-digital</type>
|
||||
<user-hidden>false</user-hidden>
|
||||
<active-input>true</active-input>
|
||||
<signal-state>valid</signal-state>
|
||||
<signal-mode>480i</signal-mode>
|
||||
<signal-quality>20</signal-quality>
|
||||
<signal-strength>-75</signal-strength>
|
||||
<program-title>Airwolf</program-title>
|
||||
<program-description>The team will travel all around the world in order to shut down a global crime ring.</program-description>
|
||||
<program-ratings>TV-14-D-V</program-ratings>
|
||||
<program-analog-audio>none</program-analog-audio>
|
||||
<program-digital-audio>stereo</program-digital-audio>
|
||||
<program-audio-languages>eng</program-audio-languages>
|
||||
<program-audio-formats>AC3</program-audio-formats>
|
||||
<program-audio-language>eng</program-audio-language>
|
||||
<program-audio-format>AC3</program-audio-format>
|
||||
<program-has-cc>true</program-has-cc>
|
||||
</channel>
|
||||
</tv-channel>
|
15
tests/fixtures/roku/rokutv-tv-channels.xml
vendored
Normal file
15
tests/fixtures/roku/rokutv-tv-channels.xml
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<tv-channels>
|
||||
<channel>
|
||||
<number>1.1</number>
|
||||
<name>WhatsOn</name>
|
||||
<type>air-digital</type>
|
||||
<user-hidden>false</user-hidden>
|
||||
</channel>
|
||||
<channel>
|
||||
<number>1.3</number>
|
||||
<name>QVC</name>
|
||||
<type>air-digital</type>
|
||||
<user-hidden>false</user-hidden>
|
||||
</channel>
|
||||
</tv-channels>
|
Loading…
Add table
Reference in a new issue