Add BROWSE_MEDIA support to frontier_silicon (#74950)
* Add BROWSE_MEDIA support to frontier_silicon * Address review comments * Don't use mediatype to differentiate between channels and presetspull/82925/head
parent
7ebd279e14
commit
ce1b2f45c7
|
@ -0,0 +1,155 @@
|
|||
"""Support for media browsing."""
|
||||
import logging
|
||||
|
||||
from afsapi import AFSAPI, FSApiException, OutOfRangeException, Preset
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
BrowseError,
|
||||
BrowseMedia,
|
||||
MediaClass,
|
||||
MediaType,
|
||||
)
|
||||
|
||||
from .const import MEDIA_CONTENT_ID_CHANNELS, MEDIA_CONTENT_ID_PRESET
|
||||
|
||||
TOP_LEVEL_DIRECTORIES = {
|
||||
MEDIA_CONTENT_ID_CHANNELS: "Channels",
|
||||
MEDIA_CONTENT_ID_PRESET: "Presets",
|
||||
}
|
||||
|
||||
FSAPI_ITEM_TYPE_TO_MEDIA_CLASS = {
|
||||
0: MediaClass.DIRECTORY,
|
||||
1: MediaClass.CHANNEL,
|
||||
2: MediaClass.CHANNEL,
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _item_preset_payload(preset: Preset, player_mode: str) -> BrowseMedia:
|
||||
"""
|
||||
Create response payload for a single media item.
|
||||
|
||||
Used by async_browse_media.
|
||||
"""
|
||||
return BrowseMedia(
|
||||
title=preset.name,
|
||||
media_class=MediaClass.CHANNEL,
|
||||
media_content_type=MediaType.CHANNEL,
|
||||
# We add 1 to the preset key to keep it in sync with the numbering shown
|
||||
# on the interface of the device
|
||||
media_content_id=f"{player_mode}/{MEDIA_CONTENT_ID_PRESET}/{int(preset.key)+1}",
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
)
|
||||
|
||||
|
||||
def _item_payload(
|
||||
key, item: dict[str, str], player_mode: str, parent_keys: list[str]
|
||||
) -> BrowseMedia:
|
||||
"""
|
||||
Create response payload for a single media item.
|
||||
|
||||
Used by async_browse_media.
|
||||
"""
|
||||
assert "label" in item or "name" in item
|
||||
assert "type" in item
|
||||
|
||||
title = item.get("label") or item.get("name") or "Unknown"
|
||||
title = title.strip()
|
||||
|
||||
media_content_id = "/".join(
|
||||
[player_mode, MEDIA_CONTENT_ID_CHANNELS, *parent_keys, key]
|
||||
)
|
||||
media_class = (
|
||||
FSAPI_ITEM_TYPE_TO_MEDIA_CLASS.get(int(item["type"])) or MediaClass.CHANNEL
|
||||
)
|
||||
|
||||
return BrowseMedia(
|
||||
title=title,
|
||||
media_class=media_class,
|
||||
media_content_type=MediaClass.CHANNEL,
|
||||
media_content_id=media_content_id,
|
||||
can_play=(media_class != MediaClass.DIRECTORY),
|
||||
can_expand=(media_class == MediaClass.DIRECTORY),
|
||||
)
|
||||
|
||||
|
||||
async def browse_top_level(current_mode, afsapi: AFSAPI):
|
||||
"""
|
||||
Create response payload to describe contents of a specific library.
|
||||
|
||||
Used by async_browse_media.
|
||||
"""
|
||||
|
||||
children = [
|
||||
BrowseMedia(
|
||||
title=name,
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_type=MediaType.CHANNELS,
|
||||
media_content_id=f"{current_mode or 'unknown'}/{top_level_media_content_id}",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
)
|
||||
for top_level_media_content_id, name in TOP_LEVEL_DIRECTORIES.items()
|
||||
]
|
||||
|
||||
library_info = BrowseMedia(
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_id="library",
|
||||
media_content_type=MediaType.CHANNELS,
|
||||
title="Media Library",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children=children,
|
||||
children_media_class=MediaClass.DIRECTORY,
|
||||
)
|
||||
|
||||
return library_info
|
||||
|
||||
|
||||
async def browse_node(
|
||||
afsapi: AFSAPI,
|
||||
media_content_type,
|
||||
media_content_id,
|
||||
):
|
||||
"""List the contents of a navigation directory (or preset list) on a Frontier Silicon device."""
|
||||
|
||||
player_mode, browse_type, *parent_keys = media_content_id.split("/")
|
||||
|
||||
title = TOP_LEVEL_DIRECTORIES.get(browse_type, "Unknown")
|
||||
|
||||
children = []
|
||||
try:
|
||||
if browse_type == MEDIA_CONTENT_ID_PRESET:
|
||||
# Return the presets
|
||||
|
||||
children = [
|
||||
_item_preset_payload(preset, player_mode=player_mode)
|
||||
for preset in await afsapi.get_presets()
|
||||
]
|
||||
|
||||
else:
|
||||
# Browse to correct folder
|
||||
await afsapi.nav_select_folder_via_path(parent_keys)
|
||||
|
||||
# Return items in this folder
|
||||
children = [
|
||||
_item_payload(key, item, player_mode, parent_keys=parent_keys)
|
||||
async for key, item in await afsapi.nav_list()
|
||||
]
|
||||
except OutOfRangeException as err:
|
||||
raise BrowseError("The requested item is out of range") from err
|
||||
except FSApiException as err:
|
||||
raise BrowseError(str(err)) from err
|
||||
|
||||
return BrowseMedia(
|
||||
title=title,
|
||||
media_content_id=media_content_id,
|
||||
media_content_type=MediaType.CHANNELS,
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children=children,
|
||||
children_media_class=MediaType.CHANNEL,
|
||||
)
|
|
@ -1,6 +1,8 @@
|
|||
"""Constants for the Frontier Silicon Media Player integration."""
|
||||
|
||||
DOMAIN = "frontier_silicon"
|
||||
|
||||
DEFAULT_PIN = "1234"
|
||||
DEFAULT_PORT = 80
|
||||
|
||||
MEDIA_CONTENT_ID_PRESET = "preset"
|
||||
MEDIA_CONTENT_ID_CHANNELS = "channels"
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from afsapi import (
|
||||
AFSAPI,
|
||||
|
@ -13,6 +14,8 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.components.media_player import (
|
||||
PLATFORM_SCHEMA,
|
||||
BrowseError,
|
||||
BrowseMedia,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
|
@ -25,7 +28,8 @@ from homeassistant.helpers.entity import DeviceInfo
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import DEFAULT_PIN, DEFAULT_PORT, DOMAIN
|
||||
from .browse_media import browse_node, browse_top_level
|
||||
from .const import DEFAULT_PIN, DEFAULT_PORT, DOMAIN, MEDIA_CONTENT_ID_PRESET
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -80,7 +84,7 @@ async def async_setup_platform(
|
|||
class AFSAPIDevice(MediaPlayerEntity):
|
||||
"""Representation of a Frontier Silicon device on the network."""
|
||||
|
||||
_attr_media_content_type: str = MediaType.MUSIC
|
||||
_attr_media_content_type: str = MediaType.CHANNEL
|
||||
|
||||
_attr_supported_features = (
|
||||
MediaPlayerEntityFeature.PAUSE
|
||||
|
@ -97,6 +101,7 @@ class AFSAPIDevice(MediaPlayerEntity):
|
|||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
| MediaPlayerEntityFeature.SELECT_SOUND_MODE
|
||||
| MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||
)
|
||||
|
||||
def __init__(self, name: str | None, afsapi: AFSAPI) -> None:
|
||||
|
@ -298,3 +303,42 @@ class AFSAPIDevice(MediaPlayerEntity):
|
|||
and (mode := self.__sound_modes_by_label.get(sound_mode)) is not None
|
||||
):
|
||||
await self.fs_device.set_eq_preset(mode)
|
||||
|
||||
async def async_browse_media(
|
||||
self, media_content_type: str | None = None, media_content_id: str | None = None
|
||||
) -> BrowseMedia:
|
||||
"""Browse media library and preset stations."""
|
||||
if not media_content_id:
|
||||
return await browse_top_level(self._attr_source, self.fs_device)
|
||||
|
||||
return await browse_node(self.fs_device, media_content_type, media_content_id)
|
||||
|
||||
async def async_play_media(
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
) -> None:
|
||||
"""Play selected media or channel."""
|
||||
if media_type != MediaType.CHANNEL:
|
||||
_LOGGER.error(
|
||||
"Got %s, but frontier_silicon only supports playing channels",
|
||||
media_type,
|
||||
)
|
||||
return
|
||||
|
||||
player_mode, media_type, *keys = media_id.split("/")
|
||||
|
||||
await self.async_select_source(player_mode) # this also powers on the device
|
||||
|
||||
if media_type == MEDIA_CONTENT_ID_PRESET:
|
||||
if len(keys) != 1:
|
||||
raise BrowseError("Presets can only have 1 level")
|
||||
|
||||
# Keys of presets are 0-based, while the list shown on the device starts from 1
|
||||
preset = int(keys[0]) - 1
|
||||
|
||||
result = await self.fs_device.select_preset(preset)
|
||||
else:
|
||||
result = await self.fs_device.nav_select_item_via_path(keys)
|
||||
|
||||
await self.async_update()
|
||||
self._attr_media_content_id = media_id
|
||||
return result
|
||||
|
|
Loading…
Reference in New Issue