mirror of
https://github.com/home-assistant/core.git
synced 2025-08-06 12:08:19 +00:00
initial
This commit is contained in:
parent
8ae52cdc4c
commit
32e6e18894
@ -27,6 +27,12 @@
|
||||
},
|
||||
"call_query": {
|
||||
"service": "mdi:database"
|
||||
},
|
||||
"search": {
|
||||
"service": "mdi:folder-search-outline"
|
||||
},
|
||||
"play": {
|
||||
"service": "mdi:folder-play-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,13 @@ from homeassistant.components.media_player import (
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY
|
||||
from homeassistant.const import ATTR_COMMAND, CONF_HOST, CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
@ -73,6 +79,8 @@ if TYPE_CHECKING:
|
||||
|
||||
SERVICE_CALL_METHOD = "call_method"
|
||||
SERVICE_CALL_QUERY = "call_query"
|
||||
SERVICE_SEARCH = "search"
|
||||
SERVICE_PLAY = "play"
|
||||
|
||||
ATTR_QUERY_RESULT = "query_result"
|
||||
|
||||
@ -80,7 +88,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
ATTR_PARAMETERS = "parameters"
|
||||
ATTR_RETURN_ITEMS = "return_items"
|
||||
ATTR_SEARCH_STRING = "search_string"
|
||||
ATTR_PLAYLIST_ACTION = "playlist_action"
|
||||
ATTR_SEARCH_TYPE = "search_type"
|
||||
ATTR_OTHER_PLAYER = "other_player"
|
||||
ATTR_TAGS = "tags"
|
||||
|
||||
|
||||
ATTR_TO_PROPERTY = [
|
||||
ATTR_QUERY_RESULT,
|
||||
@ -134,6 +148,27 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
# Register entity services
|
||||
|
||||
async def async_call_query_helper(
|
||||
entity: SqueezeBoxMediaPlayerEntity, serviceCall: ServiceCall
|
||||
) -> ServiceResponse | None:
|
||||
return await entity.async_call_query(
|
||||
serviceCall.data[ATTR_COMMAND],
|
||||
serviceCall.data.get(ATTR_PARAMETERS, []),
|
||||
serviceCall.return_response,
|
||||
)
|
||||
|
||||
async def async_search_service_helper(
|
||||
entity: SqueezeBoxMediaPlayerEntity, serviceCall: ServiceCall
|
||||
) -> ServiceResponse | None:
|
||||
return await entity.async_search_service(
|
||||
serviceCall.data[ATTR_COMMAND],
|
||||
serviceCall.data[ATTR_RETURN_ITEMS],
|
||||
serviceCall.data.get(ATTR_SEARCH_STRING),
|
||||
serviceCall.data.get(ATTR_TAGS),
|
||||
serviceCall.return_response,
|
||||
)
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_CALL_METHOD,
|
||||
@ -150,12 +185,35 @@ async def async_setup_entry(
|
||||
{
|
||||
vol.Required(ATTR_COMMAND): cv.string,
|
||||
vol.Optional(ATTR_PARAMETERS): vol.All(
|
||||
cv.ensure_list, vol.Length(min=1), [cv.string]
|
||||
cv.ensure_list,
|
||||
vol.Length(min=1),
|
||||
[cv.string],
|
||||
),
|
||||
},
|
||||
"async_call_query",
|
||||
async_call_query_helper,
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SEARCH,
|
||||
{
|
||||
vol.Required(ATTR_COMMAND): cv.string,
|
||||
vol.Required(ATTR_RETURN_ITEMS): int,
|
||||
vol.Optional(ATTR_SEARCH_STRING): cv.string,
|
||||
vol.Optional(ATTR_TAGS): cv.string,
|
||||
},
|
||||
async_search_service_helper,
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_PLAY,
|
||||
{
|
||||
vol.Required(ATTR_COMMAND): cv.string,
|
||||
vol.Required(ATTR_SEARCH_TYPE): cv.string,
|
||||
vol.Required(ATTR_SEARCH_STRING): cv.string,
|
||||
vol.Required(ATTR_PLAYLIST_ACTION): cv.string,
|
||||
},
|
||||
"async_play_service",
|
||||
)
|
||||
|
||||
# Start server discovery task if not already running
|
||||
entry.async_on_unload(async_at_start(hass, start_server_discovery))
|
||||
|
||||
@ -594,8 +652,11 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
await self._player.async_query(*all_params)
|
||||
|
||||
async def async_call_query(
|
||||
self, command: str, parameters: list[str] | None = None
|
||||
) -> None:
|
||||
self,
|
||||
command: str,
|
||||
parameters: list[str] | None = None,
|
||||
response: bool | None = False,
|
||||
) -> ServiceResponse | None:
|
||||
"""Call Squeezebox JSON/RPC method where we care about the result.
|
||||
|
||||
Additional parameters are added to the command to form the list of
|
||||
@ -604,8 +665,127 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
all_params = [command]
|
||||
if parameters:
|
||||
all_params.extend(parameters)
|
||||
self._query_result = await self._player.async_query(*all_params)
|
||||
_LOGGER.debug("call_query got result %s", self._query_result)
|
||||
query_result = await self._player.async_query(*all_params)
|
||||
_LOGGER.debug("call_query got result %s", query_result)
|
||||
if response:
|
||||
return query_result
|
||||
self._query_result = query_result
|
||||
self.async_write_ha_state()
|
||||
return None
|
||||
|
||||
async def async_search_service(
|
||||
self,
|
||||
command: str,
|
||||
return_items: int,
|
||||
search_string: str | None = None,
|
||||
tags: str | None = None,
|
||||
response: bool | None = None,
|
||||
) -> None:
|
||||
"""Call Squeezebox JSON/RPC method to search media library."""
|
||||
match command:
|
||||
case "albums":
|
||||
_param = [
|
||||
"0",
|
||||
str(return_items),
|
||||
("tags:" + tags) if tags else "tags:laay",
|
||||
"search:" + search_string if search_string is not None else "",
|
||||
]
|
||||
case "favorites":
|
||||
_param = [
|
||||
"items",
|
||||
"0",
|
||||
str(return_items),
|
||||
"search:" + search_string if search_string is not None else "",
|
||||
]
|
||||
case "artists":
|
||||
_param = [
|
||||
"0",
|
||||
str(return_items),
|
||||
("tags:" + tags) if tags else "",
|
||||
"search:" + search_string if search_string is not None else "",
|
||||
]
|
||||
case "genres":
|
||||
_param = [
|
||||
"0",
|
||||
str(return_items),
|
||||
("tags:" + tags) if tags else "",
|
||||
"search:" + search_string if search_string is not None else "",
|
||||
]
|
||||
case "tracks":
|
||||
_param = [
|
||||
"0",
|
||||
str(return_items),
|
||||
("tags:" + tags) if tags else "tags:aglQrTy",
|
||||
"search:" + search_string if search_string is not None else "",
|
||||
]
|
||||
case "playlists":
|
||||
_param = [
|
||||
"0",
|
||||
str(return_items),
|
||||
("tags:" + tags) if tags else "",
|
||||
"search:" + search_string if search_string is not None else "",
|
||||
]
|
||||
case "players":
|
||||
_param = ["0", str(return_items)]
|
||||
case _:
|
||||
_LOGGER.debug("Invalid Search Service Command")
|
||||
return None
|
||||
|
||||
return await self.async_call_query(command, _param, response)
|
||||
|
||||
async def async_play_service(
|
||||
self, command: str, search_type: str, search_string: str, playlist_action: str
|
||||
) -> None:
|
||||
"""Call Squeezebox JSON/RPC method to search media library."""
|
||||
if search_type == "text":
|
||||
match command:
|
||||
case "favorite":
|
||||
_param = [
|
||||
"favorites",
|
||||
"items",
|
||||
"0",
|
||||
"1",
|
||||
"search:" + search_string if search_string is not None else "",
|
||||
]
|
||||
result_loop = "loop_loop"
|
||||
_type = "Favorites"
|
||||
case _:
|
||||
_param = [
|
||||
command + "s",
|
||||
"items0",
|
||||
"1",
|
||||
"search:" + search_string if search_string is not None else "",
|
||||
]
|
||||
result_loop = command + "s_loop"
|
||||
_type = command
|
||||
|
||||
query_result = await self._player.async_query(*_param)
|
||||
|
||||
if query_result["count"] == 0:
|
||||
raise ServiceValidationError("Search returned zero results")
|
||||
|
||||
if query_result["count"] > 1:
|
||||
raise ServiceValidationError(
|
||||
f"Search returned {query_result['count']} results. Each search must return only one result"
|
||||
)
|
||||
|
||||
_id = str(query_result[result_loop][0]["id"])
|
||||
|
||||
else:
|
||||
_id = search_string
|
||||
|
||||
if command == "favorite":
|
||||
_type = "Favorites"
|
||||
else:
|
||||
_type = command
|
||||
|
||||
match playlist_action:
|
||||
case "add":
|
||||
await self.async_play_media(_type, _id, enqueue=MediaPlayerEnqueue.ADD)
|
||||
case "next":
|
||||
await self.async_play_media(_type, _id, enqueue=MediaPlayerEnqueue.NEXT)
|
||||
case _:
|
||||
await self.async_play_media(_type, _id)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_join_players(self, group_members: list[str]) -> None:
|
||||
|
@ -30,3 +30,82 @@ call_query:
|
||||
advanced: true
|
||||
selector:
|
||||
object:
|
||||
search:
|
||||
target:
|
||||
entity:
|
||||
integration: squeezebox
|
||||
domain: media_player
|
||||
fields:
|
||||
command:
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
translation_key: "search_command_selector"
|
||||
multiple: false
|
||||
options:
|
||||
- albums
|
||||
- artists
|
||||
- tracks
|
||||
- playlists
|
||||
- genres
|
||||
- favorites
|
||||
- players
|
||||
return_items:
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 10000
|
||||
mode: box
|
||||
search_string:
|
||||
required: false
|
||||
example: Revolver
|
||||
selector:
|
||||
text:
|
||||
tags:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
play:
|
||||
target:
|
||||
entity:
|
||||
integration: squeezebox
|
||||
domain: media_player
|
||||
fields:
|
||||
command:
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
translation_key: "play_command_selector"
|
||||
multiple: false
|
||||
options:
|
||||
- album
|
||||
- artist
|
||||
- track
|
||||
- playlist
|
||||
- genre
|
||||
- favorite
|
||||
search_type:
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
translation_key: "search_type_selector"
|
||||
multiple: false
|
||||
options:
|
||||
- text
|
||||
- item
|
||||
search_string:
|
||||
required: true
|
||||
example: Revolver
|
||||
selector:
|
||||
text:
|
||||
playlist_action:
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
translation_key: "playlist_type_selector"
|
||||
multiple: false
|
||||
options:
|
||||
- play
|
||||
- add
|
||||
- next
|
||||
|
@ -49,7 +49,7 @@
|
||||
},
|
||||
"call_query": {
|
||||
"name": "Call query",
|
||||
"description": "Calls a custom Squeezebox JSONRPC API. Result will be stored in 'query_result' attribute of the Squeezebox entity.",
|
||||
"description": "Calls a custom Squeezebox JSONRPC API. Result will be stored in 'query_result' attribute of the Squeezebox entity if no result variable is given.",
|
||||
"fields": {
|
||||
"command": {
|
||||
"name": "Command",
|
||||
@ -60,6 +60,50 @@
|
||||
"description": "[%key:component::squeezebox::services::call_method::fields::parameters::description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"name": "Search music library & players",
|
||||
"description": "Search for items in the music library & players.",
|
||||
"fields": {
|
||||
"command": {
|
||||
"name": "Items",
|
||||
"description": "The items to search for on LMS."
|
||||
},
|
||||
"return_items": {
|
||||
"name": "Number of Items",
|
||||
"description": "The number of items to return."
|
||||
},
|
||||
"search_string": {
|
||||
"name": "Search String",
|
||||
"description": "Limit the search to items matching the search string."
|
||||
},
|
||||
"tags": {
|
||||
"name": "Tags",
|
||||
"description": "Override default tags for search"
|
||||
}
|
||||
}
|
||||
},
|
||||
"play": {
|
||||
"name": "Play music",
|
||||
"description": "Play an item in the music library",
|
||||
"fields": {
|
||||
"command": {
|
||||
"name": "Items",
|
||||
"description": "The items to play on LMS."
|
||||
},
|
||||
"search_type": {
|
||||
"name": "Search Type",
|
||||
"description": "Full Text search for a string, or directly provide an item_id"
|
||||
},
|
||||
"search_string": {
|
||||
"name": "Search String",
|
||||
"description": "Search for items matching the search string or item_id."
|
||||
},
|
||||
"playlist_action": {
|
||||
"name": "Playlist Action",
|
||||
"description": "How should the item be added to the playlist"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@ -118,5 +162,41 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"search_command_selector": {
|
||||
"options": {
|
||||
"albums": "Albums",
|
||||
"artists": "Artists",
|
||||
"tracks": "Tracks",
|
||||
"playlists": "Playlists",
|
||||
"genres": "Genres",
|
||||
"favorites": "Favorites",
|
||||
"players": "Players"
|
||||
}
|
||||
},
|
||||
"play_command_selector": {
|
||||
"options": {
|
||||
"album": "Album",
|
||||
"artist": "Artist",
|
||||
"track": "Track",
|
||||
"playlist": "Playlist",
|
||||
"genre": "Genre",
|
||||
"favorite": "Favorite"
|
||||
}
|
||||
},
|
||||
"search_type_selector": {
|
||||
"options": {
|
||||
"text": "Full Text",
|
||||
"item": "Item ID"
|
||||
}
|
||||
},
|
||||
"playlist_type_selector": {
|
||||
"options": {
|
||||
"play": "Replace the current playlist and play the item",
|
||||
"add": "Add the item to the end of the current playlist",
|
||||
"next": "Play the item next, after the current item on the playlist"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -42,8 +42,11 @@ from homeassistant.components.squeezebox.const import (
|
||||
)
|
||||
from homeassistant.components.squeezebox.media_player import (
|
||||
ATTR_PARAMETERS,
|
||||
ATTR_RETURN_ITEMS,
|
||||
ATTR_SEARCH_STRING,
|
||||
SERVICE_CALL_METHOD,
|
||||
SERVICE_CALL_QUERY,
|
||||
SERVICE_SEARCH,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_COMMAND,
|
||||
@ -109,9 +112,12 @@ async def test_squeezebox_player_rediscovery(
|
||||
|
||||
# Make the player appear unavailable
|
||||
configured_player.connected = False
|
||||
freezer.tick(timedelta(seconds=PLAYER_UPDATE_INTERVAL))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "media_player.test_player"},
|
||||
blocking=True,
|
||||
)
|
||||
assert hass.states.get("media_player.test_player").state == STATE_UNAVAILABLE
|
||||
|
||||
# Make the player available again
|
||||
@ -120,7 +126,7 @@ async def test_squeezebox_player_rediscovery(
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
freezer.tick(timedelta(seconds=PLAYER_UPDATE_INTERVAL))
|
||||
freezer.tick(timedelta(seconds=SENSOR_UPDATE_INTERVAL))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("media_player.test_player").state == MediaPlayerState.IDLE
|
||||
@ -776,6 +782,150 @@ async def test_squeezebox_call_method(
|
||||
)
|
||||
|
||||
|
||||
async def test_squeezebox_search_albums(
|
||||
hass: HomeAssistant, configured_player: MagicMock
|
||||
) -> None:
|
||||
"""Test query service call."""
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SEARCH,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
ATTR_COMMAND: "albums",
|
||||
ATTR_RETURN_ITEMS: 1,
|
||||
ATTR_SEARCH_STRING: "searchstring",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
configured_player.async_query.assert_called_once_with(
|
||||
"albums", "0", "1", "tags:laay", "search:searchstring"
|
||||
)
|
||||
|
||||
|
||||
async def test_squeezebox_search_favorites(
|
||||
hass: HomeAssistant, configured_player: MagicMock
|
||||
) -> None:
|
||||
"""Test query service call."""
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SEARCH,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
ATTR_COMMAND: "favorites",
|
||||
ATTR_RETURN_ITEMS: 1,
|
||||
ATTR_SEARCH_STRING: "searchstring",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
configured_player.async_query.assert_called_once_with(
|
||||
"favorites", "items", "0", "1", "search:searchstring"
|
||||
)
|
||||
|
||||
|
||||
async def test_squeezebox_search_artists(
|
||||
hass: HomeAssistant, configured_player: MagicMock
|
||||
) -> None:
|
||||
"""Test query service call."""
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SEARCH,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
ATTR_COMMAND: "artists",
|
||||
ATTR_RETURN_ITEMS: 1,
|
||||
ATTR_SEARCH_STRING: "searchstring",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
configured_player.async_query.assert_called_once_with(
|
||||
"artists", "0", "1", "search:searchstring"
|
||||
)
|
||||
|
||||
|
||||
async def test_squeezebox_search_genres(
|
||||
hass: HomeAssistant, configured_player: MagicMock
|
||||
) -> None:
|
||||
"""Test query service call."""
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SEARCH,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
ATTR_COMMAND: "genres",
|
||||
ATTR_RETURN_ITEMS: 1,
|
||||
ATTR_SEARCH_STRING: "searchstring",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
configured_player.async_query.assert_called_once_with(
|
||||
"genres", "0", "1", "search:searchstring"
|
||||
)
|
||||
|
||||
|
||||
async def test_squeezebox_search_tracks(
|
||||
hass: HomeAssistant, configured_player: MagicMock
|
||||
) -> None:
|
||||
"""Test query service call."""
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SEARCH,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
ATTR_COMMAND: "tracks",
|
||||
ATTR_RETURN_ITEMS: 1,
|
||||
ATTR_SEARCH_STRING: "searchstring",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
configured_player.async_query.assert_called_once_with(
|
||||
"tracks", "0", "1", "tags:aglQrTy", "search:searchstring"
|
||||
)
|
||||
|
||||
|
||||
async def test_squeezebox_search_playlists(
|
||||
hass: HomeAssistant, configured_player: MagicMock
|
||||
) -> None:
|
||||
"""Test query service call."""
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SEARCH,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
ATTR_COMMAND: "playlists",
|
||||
ATTR_RETURN_ITEMS: 1,
|
||||
ATTR_SEARCH_STRING: "searchstring",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
configured_player.async_query.assert_called_once_with(
|
||||
"playlists", "0", "1", "search:searchstring"
|
||||
)
|
||||
|
||||
|
||||
async def test_squeezebox_search_players(
|
||||
hass: HomeAssistant, configured_player: MagicMock
|
||||
) -> None:
|
||||
"""Test query service call."""
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SEARCH,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
ATTR_COMMAND: "players",
|
||||
ATTR_RETURN_ITEMS: 1,
|
||||
ATTR_SEARCH_STRING: "searchstring",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
configured_player.async_query.assert_called_once_with("players", "0", "1")
|
||||
|
||||
|
||||
async def test_squeezebox_invalid_state(
|
||||
hass: HomeAssistant, configured_player: MagicMock, freezer: FrozenDateTimeFactory
|
||||
) -> None:
|
||||
|
Loading…
x
Reference in New Issue
Block a user