mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +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.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.device_registry import format_mac
|
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
|
from .helpers import create_api, get_enable_ime
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
APPS_NEW_ID = "NewApp"
|
||||||
|
CONF_APP_DELETE = "app_delete"
|
||||||
|
CONF_APP_ID = "app_id"
|
||||||
|
|
||||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required("host"): str,
|
vol.Required("host"): str,
|
||||||
@ -213,17 +223,46 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||||
"""Android TV Remote options flow."""
|
"""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(
|
async def async_step_init(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Manage the options."""
|
"""Manage the options."""
|
||||||
if user_input is not None:
|
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(
|
return self.async_show_form(
|
||||||
step_id="init",
|
step_id="init",
|
||||||
data_schema=vol.Schema(
|
data_schema=vol.Schema(
|
||||||
{
|
{
|
||||||
|
vol.Optional(CONF_APPS): SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=apps, mode=SelectSelectorMode.DROPDOWN
|
||||||
|
)
|
||||||
|
),
|
||||||
vol.Required(
|
vol.Required(
|
||||||
CONF_ENABLE_IME,
|
CONF_ENABLE_IME,
|
||||||
default=get_enable_ime(self.config_entry),
|
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"
|
DOMAIN: Final = "androidtv_remote"
|
||||||
|
|
||||||
|
CONF_APPS = "apps"
|
||||||
CONF_ENABLE_IME: Final = "enable_ime"
|
CONF_ENABLE_IME: Final = "enable_ime"
|
||||||
CONF_ENABLE_IME_DEFAULT_VALUE: Final = True
|
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 __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from androidtvremote2 import AndroidTVRemote, ConnectionClosed
|
from androidtvremote2 import AndroidTVRemote, ConnectionClosed
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
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.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import CONF_APPS, DOMAIN
|
||||||
|
|
||||||
|
|
||||||
class AndroidTVRemoteBaseEntity(Entity):
|
class AndroidTVRemoteBaseEntity(Entity):
|
||||||
@ -26,6 +28,7 @@ class AndroidTVRemoteBaseEntity(Entity):
|
|||||||
self._api = api
|
self._api = api
|
||||||
self._host = config_entry.data[CONF_HOST]
|
self._host = config_entry.data[CONF_HOST]
|
||||||
self._name = config_entry.data[CONF_NAME]
|
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_unique_id = config_entry.unique_id
|
||||||
self._attr_is_on = api.is_on
|
self._attr_is_on = api.is_on
|
||||||
device_info = api.device_info
|
device_info = api.device_info
|
||||||
|
@ -8,17 +8,20 @@ from typing import Any
|
|||||||
from androidtvremote2 import AndroidTVRemote, ConnectionClosed
|
from androidtvremote2 import AndroidTVRemote, ConnectionClosed
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
|
MediaClass,
|
||||||
MediaPlayerDeviceClass,
|
MediaPlayerDeviceClass,
|
||||||
MediaPlayerEntity,
|
MediaPlayerEntity,
|
||||||
MediaPlayerEntityFeature,
|
MediaPlayerEntityFeature,
|
||||||
MediaPlayerState,
|
MediaPlayerState,
|
||||||
MediaType,
|
MediaType,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.media_player.browse_media import BrowseMedia
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import AndroidTVRemoteConfigEntry
|
from . import AndroidTVRemoteConfigEntry
|
||||||
|
from .const import CONF_APP_ICON, CONF_APP_NAME
|
||||||
from .entity import AndroidTVRemoteBaseEntity
|
from .entity import AndroidTVRemoteBaseEntity
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
@ -50,6 +53,7 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
|
|||||||
| MediaPlayerEntityFeature.PLAY
|
| MediaPlayerEntityFeature.PLAY
|
||||||
| MediaPlayerEntityFeature.STOP
|
| MediaPlayerEntityFeature.STOP
|
||||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||||
|
| MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -65,7 +69,11 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
|
|||||||
def _update_current_app(self, current_app: str) -> None:
|
def _update_current_app(self, current_app: str) -> None:
|
||||||
"""Update current app info."""
|
"""Update current app info."""
|
||||||
self._attr_app_id = current_app
|
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:
|
def _update_volume_info(self, volume_info: dict[str, str | bool]) -> None:
|
||||||
"""Update volume info."""
|
"""Update volume info."""
|
||||||
@ -176,12 +184,41 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
|
|||||||
await self._channel_set_task
|
await self._channel_set_task
|
||||||
return
|
return
|
||||||
|
|
||||||
if media_type == MediaType.URL:
|
if media_type in [MediaType.URL, MediaType.APP]:
|
||||||
self._send_launch_app_command(media_id)
|
self._send_launch_app_command(media_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
raise ValueError(f"Invalid media type: {media_type}")
|
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(
|
async def _send_key_commands(
|
||||||
self, key_codes: list[str], delay_secs: float = 0.1
|
self, key_codes: list[str], delay_secs: float = 0.1
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import AndroidTVRemoteConfigEntry
|
from . import AndroidTVRemoteConfigEntry
|
||||||
|
from .const import CONF_APP_NAME
|
||||||
from .entity import AndroidTVRemoteBaseEntity
|
from .entity import AndroidTVRemoteBaseEntity
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
@ -41,17 +42,28 @@ class AndroidTVRemoteEntity(AndroidTVRemoteBaseEntity, RemoteEntity):
|
|||||||
|
|
||||||
_attr_supported_features = RemoteEntityFeature.ACTIVITY
|
_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
|
@callback
|
||||||
def _current_app_updated(self, current_app: str) -> None:
|
def _current_app_updated(self, current_app: str) -> None:
|
||||||
"""Update the state when the current app changes."""
|
"""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()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
await super().async_added_to_hass()
|
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)
|
self._api.add_current_app_updated_callback(self._current_app_updated)
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
@ -66,6 +78,14 @@ class AndroidTVRemoteEntity(AndroidTVRemoteBaseEntity, RemoteEntity):
|
|||||||
self._send_key_command("POWER")
|
self._send_key_command("POWER")
|
||||||
activity = kwargs.get(ATTR_ACTIVITY, "")
|
activity = kwargs.get(ATTR_ACTIVITY, "")
|
||||||
if 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)
|
self._send_launch_app_command(activity)
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
@ -39,8 +39,19 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"data": {
|
"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."
|
"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 import config_entries
|
||||||
from homeassistant.components import zeroconf
|
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.config_entries import ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
@ -886,14 +897,14 @@ async def test_options_flow(
|
|||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "init"
|
assert result["step_id"] == "init"
|
||||||
data_schema = result["data_schema"].schema
|
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 = await hass.config_entries.options.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
user_input={"enable_ime": False},
|
user_input={CONF_ENABLE_IME: False},
|
||||||
)
|
)
|
||||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert mock_api.disconnect.call_count == 1
|
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_init(mock_config_entry.entry_id)
|
||||||
result = await hass.config_entries.options.async_configure(
|
result = await hass.config_entries.options.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
user_input={"enable_ime": False},
|
user_input={CONF_ENABLE_IME: False},
|
||||||
)
|
)
|
||||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert mock_api.disconnect.call_count == 1
|
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_init(mock_config_entry.entry_id)
|
||||||
result = await hass.config_entries.options.async_configure(
|
result = await hass.config_entries.options.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
user_input={"enable_ime": True},
|
user_input={CONF_ENABLE_IME: True},
|
||||||
)
|
)
|
||||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert mock_api.disconnect.call_count == 2
|
assert mock_api.disconnect.call_count == 2
|
||||||
assert mock_api.async_connect.call_count == 3
|
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 homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
MEDIA_PLAYER_ENTITY = "media_player.my_android_tv"
|
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
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test the Android TV Remote media player receives push updates and state is updated."""
|
"""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)
|
mock_config_entry.add_to_hass(hass)
|
||||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||||
@ -39,6 +43,13 @@ async def test_media_player_receives_push_updates(
|
|||||||
== "com.google.android.tvlauncher"
|
== "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})
|
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
|
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")
|
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):
|
with pytest.raises(ValueError):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"media_player",
|
"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(
|
async def test_media_player_connection_closed(
|
||||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -19,6 +19,9 @@ async def test_remote_receives_push_updates(
|
|||||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test the Android TV Remote receives push updates and state is updated."""
|
"""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)
|
mock_config_entry.add_to_hass(hass)
|
||||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
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"
|
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)
|
mock_api._on_is_available_updated(False)
|
||||||
assert hass.states.is_state(REMOTE_ENTITY, STATE_UNAVAILABLE)
|
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
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test the Android TV Remote toggles."""
|
"""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)
|
mock_config_entry.add_to_hass(hass)
|
||||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
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_key_command.call_count == 2
|
||||||
assert mock_api.send_launch_app_command.call_count == 1
|
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(
|
async def test_remote_send_command(
|
||||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
||||||
|
Loading…
x
Reference in New Issue
Block a user