Fix playing media via roku (#128133)
* re-support playing media via roku * fixes * test fixes * Update test_media_player.py * always send media type * add description to options flow
This commit is contained in:
parent
f47a012c62
commit
cb1e5a2412
8 changed files with 138 additions and 30 deletions
|
@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
|
|||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID, DOMAIN
|
||||
from .coordinator import RokuDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [
|
||||
|
@ -24,7 +24,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
device_id = entry.entry_id
|
||||
|
||||
coordinator = RokuDataUpdateCoordinator(
|
||||
hass, host=entry.data[CONF_HOST], device_id=device_id
|
||||
hass,
|
||||
host=entry.data[CONF_HOST],
|
||||
device_id=device_id,
|
||||
play_media_app_id=entry.options.get(
|
||||
CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID
|
||||
),
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
@ -32,6 +37,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -40,3 +47,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Reload the config entry when it changed."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
|
|
@ -10,12 +10,17 @@ from rokuecp import Roku, RokuError
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import ssdp, zeroconf
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID, DOMAIN
|
||||
|
||||
DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
|
||||
|
||||
|
@ -155,3 +160,36 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
title=self.discovery_info[CONF_NAME],
|
||||
data=self.discovery_info,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowWithConfigEntry:
|
||||
"""Create the options flow."""
|
||||
return RokuOptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class RokuOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
"""Handle Roku options."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage Roku options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_PLAY_MEDIA_APP_ID,
|
||||
default=self.options.get(
|
||||
CONF_PLAY_MEDIA_APP_ID, DEFAULT_PLAY_MEDIA_APP_ID
|
||||
),
|
||||
): str,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
|
|
@ -15,3 +15,9 @@ DEFAULT_PORT = 8060
|
|||
|
||||
# Services
|
||||
SERVICE_SEARCH = "search"
|
||||
|
||||
# Config
|
||||
CONF_PLAY_MEDIA_APP_ID = "play_media_app_id"
|
||||
|
||||
# Defaults
|
||||
DEFAULT_PLAY_MEDIA_APP_ID = "15985"
|
||||
|
|
|
@ -29,15 +29,12 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]):
|
|||
roku: Roku
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
host: str,
|
||||
device_id: str,
|
||||
self, hass: HomeAssistant, *, host: str, device_id: str, play_media_app_id: str
|
||||
) -> None:
|
||||
"""Initialize global Roku data updater."""
|
||||
self.device_id = device_id
|
||||
self.roku = Roku(host=host, session=async_get_clientsession(hass))
|
||||
self.play_media_app_id = play_media_app_id
|
||||
|
||||
self.full_update_interval = timedelta(minutes=15)
|
||||
self.last_full_update = None
|
||||
|
|
|
@ -445,17 +445,25 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
|||
if attr in extra
|
||||
}
|
||||
|
||||
params = {"t": "a", **params}
|
||||
params = {"u": media_id, "t": "a", **params}
|
||||
|
||||
await self.coordinator.roku.play_on_roku(media_id, params)
|
||||
await self.coordinator.roku.launch(
|
||||
self.coordinator.play_media_app_id,
|
||||
params,
|
||||
)
|
||||
elif media_type in {MediaType.URL, MediaType.VIDEO}:
|
||||
params = {
|
||||
param: extra[attr]
|
||||
for (attr, param) in ATTRS_TO_PLAY_ON_ROKU_PARAMS.items()
|
||||
if attr in extra
|
||||
}
|
||||
params["u"] = media_id
|
||||
params["t"] = "v"
|
||||
|
||||
await self.coordinator.roku.play_on_roku(media_id, params)
|
||||
await self.coordinator.roku.launch(
|
||||
self.coordinator.play_media_app_id,
|
||||
params,
|
||||
)
|
||||
else:
|
||||
_LOGGER.error("Media type %s is not supported", original_media_type)
|
||||
return
|
||||
|
|
|
@ -24,6 +24,18 @@
|
|||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"play_media_app_id": "Play Media Roku Application ID"
|
||||
},
|
||||
"data_description": {
|
||||
"play_media_app_id": "The application ID to use when launching media playback. Must support the PlayOnRoku API."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"headphones_connected": {
|
||||
|
|
|
@ -6,7 +6,7 @@ from unittest.mock import MagicMock
|
|||
import pytest
|
||||
from rokuecp import RokuConnectionError
|
||||
|
||||
from homeassistant.components.roku.const import DOMAIN
|
||||
from homeassistant.components.roku.const import CONF_PLAY_MEDIA_APP_ID, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_HOMEKIT, SOURCE_SSDP, SOURCE_USER
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
@ -254,3 +254,25 @@ async def test_ssdp_discovery(
|
|||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == HOST
|
||||
assert result["data"][CONF_NAME] == UPNP_FRIENDLY_NAME
|
||||
|
||||
|
||||
async def test_options_flow(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test options config flow."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
|
||||
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("step_id") == "init"
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_PLAY_MEDIA_APP_ID: "782875"},
|
||||
)
|
||||
|
||||
assert result2.get("type") is FlowResultType.CREATE_ENTRY
|
||||
assert result2.get("data") == {
|
||||
CONF_PLAY_MEDIA_APP_ID: "782875",
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ from homeassistant.components.roku.const import (
|
|||
ATTR_FORMAT,
|
||||
ATTR_KEYWORD,
|
||||
ATTR_MEDIA_TYPE,
|
||||
DEFAULT_PLAY_MEDIA_APP_ID,
|
||||
DOMAIN,
|
||||
SERVICE_SEARCH,
|
||||
)
|
||||
|
@ -495,7 +496,7 @@ async def test_services_play_media(
|
|||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_roku.play_on_roku.call_count == 0
|
||||
assert mock_roku.launch.call_count == 0
|
||||
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
|
@ -509,7 +510,7 @@ async def test_services_play_media(
|
|||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_roku.play_on_roku.call_count == 0
|
||||
assert mock_roku.launch.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -546,9 +547,10 @@ async def test_services_play_media_audio(
|
|||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_roku.play_on_roku.assert_called_once_with(
|
||||
content_id,
|
||||
mock_roku.launch.assert_called_once_with(
|
||||
DEFAULT_PLAY_MEDIA_APP_ID,
|
||||
{
|
||||
"u": content_id,
|
||||
"t": "a",
|
||||
"songName": resolved_name,
|
||||
"songFormat": resolved_format,
|
||||
|
@ -591,9 +593,11 @@ async def test_services_play_media_video(
|
|||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_roku.play_on_roku.assert_called_once_with(
|
||||
content_id,
|
||||
mock_roku.launch.assert_called_once_with(
|
||||
DEFAULT_PLAY_MEDIA_APP_ID,
|
||||
{
|
||||
"u": content_id,
|
||||
"t": "v",
|
||||
"videoName": resolved_name,
|
||||
"videoFormat": resolved_format,
|
||||
},
|
||||
|
@ -617,10 +621,12 @@ async def test_services_camera_play_stream(
|
|||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_roku.play_on_roku.call_count == 1
|
||||
mock_roku.play_on_roku.assert_called_with(
|
||||
"https://awesome.tld/api/hls/api_token/master_playlist.m3u8",
|
||||
assert mock_roku.launch.call_count == 1
|
||||
mock_roku.launch.assert_called_with(
|
||||
DEFAULT_PLAY_MEDIA_APP_ID,
|
||||
{
|
||||
"u": "https://awesome.tld/api/hls/api_token/master_playlist.m3u8",
|
||||
"t": "v",
|
||||
"videoName": "Camera Stream",
|
||||
"videoFormat": "hls",
|
||||
},
|
||||
|
@ -653,14 +659,21 @@ async def test_services_play_media_local_source(
|
|||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_roku.play_on_roku.call_count == 1
|
||||
assert mock_roku.play_on_roku.call_args
|
||||
call_args = mock_roku.play_on_roku.call_args.args
|
||||
assert "/local/Epic%20Sax%20Guy%2010%20Hours.mp4?authSig=" in call_args[0]
|
||||
assert call_args[1] == {
|
||||
"videoFormat": "mp4",
|
||||
"videoName": "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4",
|
||||
}
|
||||
assert mock_roku.launch.call_count == 1
|
||||
assert mock_roku.launch.call_args
|
||||
call_args = mock_roku.launch.call_args.args
|
||||
assert call_args[0] == DEFAULT_PLAY_MEDIA_APP_ID
|
||||
assert "u" in call_args[1]
|
||||
assert "/local/Epic%20Sax%20Guy%2010%20Hours.mp4?authSig=" in call_args[1]["u"]
|
||||
assert "t" in call_args[1]
|
||||
assert call_args[1]["t"] == "v"
|
||||
assert "videoFormat" in call_args[1]
|
||||
assert call_args[1]["videoFormat"] == "mp4"
|
||||
assert "videoName" in call_args[1]
|
||||
assert (
|
||||
call_args[1]["videoName"]
|
||||
== "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True)
|
||||
|
|
Loading…
Add table
Reference in a new issue