mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Implement Android TV Remote browse media with apps and activity list (#117126)
This commit is contained in:
parent
c13efa3664
commit
ba7388546e
@ -24,12 +24,22 @@ from homeassistant.config_entries import (
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import CONF_ENABLE_IME, DOMAIN
|
||||
from .const import CONF_APP_ICON, CONF_APP_NAME, CONF_APPS, CONF_ENABLE_IME, DOMAIN
|
||||
from .helpers import create_api, get_enable_ime
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
APPS_NEW_ID = "NewApp"
|
||||
CONF_APP_DELETE = "app_delete"
|
||||
CONF_APP_ID = "app_id"
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("host"): str,
|
||||
@ -213,17 +223,46 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
"""Android TV Remote options flow."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
super().__init__(config_entry)
|
||||
self._apps: dict[str, Any] = self.options.setdefault(CONF_APPS, {})
|
||||
self._conf_app_id: str | None = None
|
||||
|
||||
@callback
|
||||
def _save_config(self, data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Save the updated options."""
|
||||
new_data = {k: v for k, v in data.items() if k not in [CONF_APPS]}
|
||||
if self._apps:
|
||||
new_data[CONF_APPS] = self._apps
|
||||
|
||||
return self.async_create_entry(title="", data=new_data)
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
if sel_app := user_input.get(CONF_APPS):
|
||||
return await self.async_step_apps(None, sel_app)
|
||||
return self._save_config(user_input)
|
||||
|
||||
apps_list = {
|
||||
k: f"{v[CONF_APP_NAME]} ({k})" if CONF_APP_NAME in v else k
|
||||
for k, v in self._apps.items()
|
||||
}
|
||||
apps = [SelectOptionDict(value=APPS_NEW_ID, label="Add new")] + [
|
||||
SelectOptionDict(value=k, label=v) for k, v in apps_list.items()
|
||||
]
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_APPS): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=apps, mode=SelectSelectorMode.DROPDOWN
|
||||
)
|
||||
),
|
||||
vol.Required(
|
||||
CONF_ENABLE_IME,
|
||||
default=get_enable_ime(self.config_entry),
|
||||
@ -231,3 +270,61 @@ class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_apps(
|
||||
self, user_input: dict[str, Any] | None = None, app_id: str | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle options flow for apps list."""
|
||||
if app_id is not None:
|
||||
self._conf_app_id = app_id if app_id != APPS_NEW_ID else None
|
||||
return self._async_apps_form(app_id)
|
||||
|
||||
if user_input is not None:
|
||||
app_id = user_input.get(CONF_APP_ID, self._conf_app_id)
|
||||
if app_id:
|
||||
if user_input.get(CONF_APP_DELETE, False):
|
||||
self._apps.pop(app_id)
|
||||
else:
|
||||
self._apps[app_id] = {
|
||||
CONF_APP_NAME: user_input.get(CONF_APP_NAME, ""),
|
||||
CONF_APP_ICON: user_input.get(CONF_APP_ICON, ""),
|
||||
}
|
||||
|
||||
return await self.async_step_init()
|
||||
|
||||
@callback
|
||||
def _async_apps_form(self, app_id: str) -> ConfigFlowResult:
|
||||
"""Return configuration form for apps."""
|
||||
|
||||
app_schema = {
|
||||
vol.Optional(
|
||||
CONF_APP_NAME,
|
||||
description={
|
||||
"suggested_value": self._apps[app_id].get(CONF_APP_NAME, "")
|
||||
if app_id in self._apps
|
||||
else ""
|
||||
},
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_APP_ICON,
|
||||
description={
|
||||
"suggested_value": self._apps[app_id].get(CONF_APP_ICON, "")
|
||||
if app_id in self._apps
|
||||
else ""
|
||||
},
|
||||
): str,
|
||||
}
|
||||
if app_id == APPS_NEW_ID:
|
||||
data_schema = vol.Schema({**app_schema, vol.Optional(CONF_APP_ID): str})
|
||||
else:
|
||||
data_schema = vol.Schema(
|
||||
{**app_schema, vol.Optional(CONF_APP_DELETE, default=False): bool}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="apps",
|
||||
data_schema=data_schema,
|
||||
description_placeholders={
|
||||
"app_id": f"`{app_id}`" if app_id != APPS_NEW_ID else "",
|
||||
},
|
||||
)
|
||||
|
@ -6,5 +6,8 @@ from typing import Final
|
||||
|
||||
DOMAIN: Final = "androidtv_remote"
|
||||
|
||||
CONF_APPS = "apps"
|
||||
CONF_ENABLE_IME: Final = "enable_ime"
|
||||
CONF_ENABLE_IME_DEFAULT_VALUE: Final = True
|
||||
CONF_APP_NAME = "app_name"
|
||||
CONF_APP_ICON = "app_icon"
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from androidtvremote2 import AndroidTVRemote, ConnectionClosed
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@ -11,7 +13,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_APPS, DOMAIN
|
||||
|
||||
|
||||
class AndroidTVRemoteBaseEntity(Entity):
|
||||
@ -26,6 +28,7 @@ class AndroidTVRemoteBaseEntity(Entity):
|
||||
self._api = api
|
||||
self._host = config_entry.data[CONF_HOST]
|
||||
self._name = config_entry.data[CONF_NAME]
|
||||
self._apps: dict[str, Any] = config_entry.options.get(CONF_APPS, {})
|
||||
self._attr_unique_id = config_entry.unique_id
|
||||
self._attr_is_on = api.is_on
|
||||
device_info = api.device_info
|
||||
|
@ -8,17 +8,20 @@ from typing import Any
|
||||
from androidtvremote2 import AndroidTVRemote, ConnectionClosed
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaClass,
|
||||
MediaPlayerDeviceClass,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
MediaType,
|
||||
)
|
||||
from homeassistant.components.media_player.browse_media import BrowseMedia
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AndroidTVRemoteConfigEntry
|
||||
from .const import CONF_APP_ICON, CONF_APP_NAME
|
||||
from .entity import AndroidTVRemoteBaseEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
@ -50,6 +53,7 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
|
||||
| MediaPlayerEntityFeature.PLAY
|
||||
| MediaPlayerEntityFeature.STOP
|
||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||
| MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@ -65,7 +69,11 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
|
||||
def _update_current_app(self, current_app: str) -> None:
|
||||
"""Update current app info."""
|
||||
self._attr_app_id = current_app
|
||||
self._attr_app_name = current_app
|
||||
self._attr_app_name = (
|
||||
self._apps[current_app].get(CONF_APP_NAME, current_app)
|
||||
if current_app in self._apps
|
||||
else current_app
|
||||
)
|
||||
|
||||
def _update_volume_info(self, volume_info: dict[str, str | bool]) -> None:
|
||||
"""Update volume info."""
|
||||
@ -176,12 +184,41 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
|
||||
await self._channel_set_task
|
||||
return
|
||||
|
||||
if media_type == MediaType.URL:
|
||||
if media_type in [MediaType.URL, MediaType.APP]:
|
||||
self._send_launch_app_command(media_id)
|
||||
return
|
||||
|
||||
raise ValueError(f"Invalid media type: {media_type}")
|
||||
|
||||
async def async_browse_media(
|
||||
self,
|
||||
media_content_type: MediaType | str | None = None,
|
||||
media_content_id: str | None = None,
|
||||
) -> BrowseMedia:
|
||||
"""Browse apps."""
|
||||
children = [
|
||||
BrowseMedia(
|
||||
media_class=MediaClass.APP,
|
||||
media_content_type=MediaType.APP,
|
||||
media_content_id=app_id,
|
||||
title=app.get(CONF_APP_NAME, ""),
|
||||
thumbnail=app.get(CONF_APP_ICON, ""),
|
||||
can_play=False,
|
||||
can_expand=False,
|
||||
)
|
||||
for app_id, app in self._apps.items()
|
||||
]
|
||||
return BrowseMedia(
|
||||
title="Applications",
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_id="apps",
|
||||
media_content_type=MediaType.APPS,
|
||||
children_media_class=MediaClass.APP,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children=children,
|
||||
)
|
||||
|
||||
async def _send_key_commands(
|
||||
self, key_codes: list[str], delay_secs: float = 0.1
|
||||
) -> None:
|
||||
|
@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AndroidTVRemoteConfigEntry
|
||||
from .const import CONF_APP_NAME
|
||||
from .entity import AndroidTVRemoteBaseEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
@ -41,17 +42,28 @@ class AndroidTVRemoteEntity(AndroidTVRemoteBaseEntity, RemoteEntity):
|
||||
|
||||
_attr_supported_features = RemoteEntityFeature.ACTIVITY
|
||||
|
||||
def _update_current_app(self, current_app: str) -> None:
|
||||
"""Update current app info."""
|
||||
self._attr_current_activity = (
|
||||
self._apps[current_app].get(CONF_APP_NAME, current_app)
|
||||
if current_app in self._apps
|
||||
else current_app
|
||||
)
|
||||
|
||||
@callback
|
||||
def _current_app_updated(self, current_app: str) -> None:
|
||||
"""Update the state when the current app changes."""
|
||||
self._attr_current_activity = current_app
|
||||
self._update_current_app(current_app)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
self._attr_current_activity = self._api.current_app
|
||||
self._attr_activity_list = [
|
||||
app.get(CONF_APP_NAME, "") for app in self._apps.values()
|
||||
]
|
||||
self._update_current_app(self._api.current_app)
|
||||
self._api.add_current_app_updated_callback(self._current_app_updated)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
@ -66,6 +78,14 @@ class AndroidTVRemoteEntity(AndroidTVRemoteBaseEntity, RemoteEntity):
|
||||
self._send_key_command("POWER")
|
||||
activity = kwargs.get(ATTR_ACTIVITY, "")
|
||||
if activity:
|
||||
activity = next(
|
||||
(
|
||||
app_id
|
||||
for app_id, app in self._apps.items()
|
||||
if app.get(CONF_APP_NAME, "") == activity
|
||||
),
|
||||
activity,
|
||||
)
|
||||
self._send_launch_app_command(activity)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
|
@ -39,8 +39,19 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"apps": "Configure applications list",
|
||||
"enable_ime": "Enable IME. Needed for getting the current app. Disable for devices that show 'Use keyboard on mobile device screen' instead of the on screen keyboard."
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
"title": "Configure Android Apps",
|
||||
"description": "Configure application id {app_id}",
|
||||
"data": {
|
||||
"app_name": "Application Name",
|
||||
"app_id": "Application ID",
|
||||
"app_icon": "Application Icon",
|
||||
"app_delete": "Check to delete this application"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,18 @@ from androidtvremote2 import CannotConnect, ConnectionClosed, InvalidAuth
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.androidtv_remote.const import DOMAIN
|
||||
from homeassistant.components.androidtv_remote.config_flow import (
|
||||
APPS_NEW_ID,
|
||||
CONF_APP_DELETE,
|
||||
CONF_APP_ID,
|
||||
)
|
||||
from homeassistant.components.androidtv_remote.const import (
|
||||
CONF_APP_ICON,
|
||||
CONF_APP_NAME,
|
||||
CONF_APPS,
|
||||
CONF_ENABLE_IME,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
@ -886,14 +897,14 @@ async def test_options_flow(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
data_schema = result["data_schema"].schema
|
||||
assert set(data_schema) == {"enable_ime"}
|
||||
assert set(data_schema) == {CONF_APPS, CONF_ENABLE_IME}
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"enable_ime": False},
|
||||
user_input={CONF_ENABLE_IME: False},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert mock_config_entry.options == {"enable_ime": False}
|
||||
assert mock_config_entry.options == {CONF_ENABLE_IME: False}
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_api.disconnect.call_count == 1
|
||||
@ -903,10 +914,10 @@ async def test_options_flow(
|
||||
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"enable_ime": False},
|
||||
user_input={CONF_ENABLE_IME: False},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert mock_config_entry.options == {"enable_ime": False}
|
||||
assert mock_config_entry.options == {CONF_ENABLE_IME: False}
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_api.disconnect.call_count == 1
|
||||
@ -916,11 +927,92 @@ async def test_options_flow(
|
||||
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"enable_ime": True},
|
||||
user_input={CONF_ENABLE_IME: True},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert mock_config_entry.options == {"enable_ime": True}
|
||||
assert mock_config_entry.options == {CONF_ENABLE_IME: True}
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_api.disconnect.call_count == 2
|
||||
assert mock_api.async_connect.call_count == 3
|
||||
|
||||
# test app form with new app
|
||||
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_APPS: APPS_NEW_ID,
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "apps"
|
||||
|
||||
# test save value for new app
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_APP_ID: "app1",
|
||||
CONF_APP_NAME: "App1",
|
||||
CONF_APP_ICON: "Icon1",
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
# test app form with existing app
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_APPS: "app1",
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "apps"
|
||||
|
||||
# test change value in apps form
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_APP_NAME: "Application1",
|
||||
CONF_APP_ICON: "Icon1",
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert mock_config_entry.options == {
|
||||
CONF_APPS: {"app1": {CONF_APP_NAME: "Application1", CONF_APP_ICON: "Icon1"}},
|
||||
CONF_ENABLE_IME: True,
|
||||
}
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# test app form for delete
|
||||
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_APPS: "app1",
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "apps"
|
||||
|
||||
# test delete app1
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_APP_DELETE: True,
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert mock_config_entry.options == {CONF_ENABLE_IME: True}
|
||||
|
@ -11,6 +11,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
MEDIA_PLAYER_ENTITY = "media_player.my_android_tv"
|
||||
|
||||
@ -19,6 +20,9 @@ async def test_media_player_receives_push_updates(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
||||
) -> None:
|
||||
"""Test the Android TV Remote media player receives push updates and state is updated."""
|
||||
mock_config_entry.options = {
|
||||
"apps": {"com.google.android.youtube.tv": {"app_name": "YouTube"}}
|
||||
}
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
@ -39,6 +43,13 @@ async def test_media_player_receives_push_updates(
|
||||
== "com.google.android.tvlauncher"
|
||||
)
|
||||
|
||||
mock_api._on_current_app_updated("com.google.android.youtube.tv")
|
||||
assert (
|
||||
hass.states.get(MEDIA_PLAYER_ENTITY).attributes.get("app_id")
|
||||
== "com.google.android.youtube.tv"
|
||||
)
|
||||
assert hass.states.get(MEDIA_PLAYER_ENTITY).attributes.get("app_name") == "YouTube"
|
||||
|
||||
mock_api._on_volume_info_updated({"level": 35, "muted": False, "max": 100})
|
||||
assert hass.states.get(MEDIA_PLAYER_ENTITY).attributes.get("volume_level") == 0.35
|
||||
|
||||
@ -267,6 +278,18 @@ async def test_media_player_play_media(
|
||||
)
|
||||
mock_api.send_launch_app_command.assert_called_with("https://www.youtube.com")
|
||||
|
||||
await hass.services.async_call(
|
||||
"media_player",
|
||||
"play_media",
|
||||
{
|
||||
"entity_id": MEDIA_PLAYER_ENTITY,
|
||||
"media_content_type": "app",
|
||||
"media_content_id": "tv.twitch.android.app",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_api.send_launch_app_command.assert_called_with("tv.twitch.android.app")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await hass.services.async_call(
|
||||
"media_player",
|
||||
@ -292,6 +315,71 @@ async def test_media_player_play_media(
|
||||
)
|
||||
|
||||
|
||||
async def test_browse_media(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_api: MagicMock,
|
||||
) -> None:
|
||||
"""Test the Android TV Remote media player browse media."""
|
||||
mock_config_entry.options = {
|
||||
"apps": {
|
||||
"com.google.android.youtube.tv": {
|
||||
"app_name": "YouTube",
|
||||
"app_icon": "https://www.youtube.com/icon.png",
|
||||
},
|
||||
"org.xbmc.kodi": {"app_name": "Kodi"},
|
||||
}
|
||||
}
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
client = await hass_ws_client()
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "media_player/browse_media",
|
||||
"entity_id": MEDIA_PLAYER_ENTITY,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert {
|
||||
"title": "Applications",
|
||||
"media_class": "directory",
|
||||
"media_content_type": "apps",
|
||||
"media_content_id": "apps",
|
||||
"children_media_class": "app",
|
||||
"can_play": False,
|
||||
"can_expand": True,
|
||||
"thumbnail": None,
|
||||
"not_shown": 0,
|
||||
"children": [
|
||||
{
|
||||
"title": "YouTube",
|
||||
"media_class": "app",
|
||||
"media_content_type": "app",
|
||||
"media_content_id": "com.google.android.youtube.tv",
|
||||
"children_media_class": None,
|
||||
"can_play": False,
|
||||
"can_expand": False,
|
||||
"thumbnail": "https://www.youtube.com/icon.png",
|
||||
},
|
||||
{
|
||||
"title": "Kodi",
|
||||
"media_class": "app",
|
||||
"media_content_type": "app",
|
||||
"media_content_id": "org.xbmc.kodi",
|
||||
"children_media_class": None,
|
||||
"can_play": False,
|
||||
"can_expand": False,
|
||||
"thumbnail": "",
|
||||
},
|
||||
],
|
||||
} == response["result"]
|
||||
|
||||
|
||||
async def test_media_player_connection_closed(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
||||
) -> None:
|
||||
|
@ -19,6 +19,9 @@ async def test_remote_receives_push_updates(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
||||
) -> None:
|
||||
"""Test the Android TV Remote receives push updates and state is updated."""
|
||||
mock_config_entry.options = {
|
||||
"apps": {"com.google.android.youtube.tv": {"app_name": "YouTube"}}
|
||||
}
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
@ -34,6 +37,11 @@ async def test_remote_receives_push_updates(
|
||||
hass.states.get(REMOTE_ENTITY).attributes.get("current_activity") == "activity1"
|
||||
)
|
||||
|
||||
mock_api._on_current_app_updated("com.google.android.youtube.tv")
|
||||
assert (
|
||||
hass.states.get(REMOTE_ENTITY).attributes.get("current_activity") == "YouTube"
|
||||
)
|
||||
|
||||
mock_api._on_is_available_updated(False)
|
||||
assert hass.states.is_state(REMOTE_ENTITY, STATE_UNAVAILABLE)
|
||||
|
||||
@ -45,6 +53,9 @@ async def test_remote_toggles(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
||||
) -> None:
|
||||
"""Test the Android TV Remote toggles."""
|
||||
mock_config_entry.options = {
|
||||
"apps": {"com.google.android.youtube.tv": {"app_name": "YouTube"}}
|
||||
}
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
@ -81,6 +92,17 @@ async def test_remote_toggles(
|
||||
assert mock_api.send_key_command.call_count == 2
|
||||
assert mock_api.send_launch_app_command.call_count == 1
|
||||
|
||||
await hass.services.async_call(
|
||||
"remote",
|
||||
"turn_on",
|
||||
{"entity_id": REMOTE_ENTITY, "activity": "YouTube"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_api.send_key_command.send_launch_app_command("com.google.android.youtube.tv")
|
||||
assert mock_api.send_key_command.call_count == 2
|
||||
assert mock_api.send_launch_app_command.call_count == 2
|
||||
|
||||
|
||||
async def test_remote_send_command(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
||||
|
Loading…
x
Reference in New Issue
Block a user