diff --git a/README.md b/README.md index 3914c1b..3c323bf 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,20 @@ Alternatively: After this integration is set up, you can configure the integration to connect to the Kinetic Oasis cloud API. This will allow pulling in certain details (such as track name and image) that are otherwise not available. +# Actions + +The media player entity supports various actions, including managing the playlist queue. You can specify a track by its ID or name. If using a track name, it must match an entry in the [tracks list](custom_components/oasis_mini/pyoasismini/tracks.json). To specify multiple tracks, separate them with commas. An example is below: + +```yaml +action: media_player.play_media +target: + entity_id: media_player.oasis_mini +data: + media_content_id: 63, Turtle + media_content_type: track + enqueue: replace +``` + --- ## Support Me diff --git a/custom_components/oasis_mini/helpers.py b/custom_components/oasis_mini/helpers.py index d908c1a..c75cd91 100755 --- a/custom_components/oasis_mini/helpers.py +++ b/custom_components/oasis_mini/helpers.py @@ -2,11 +2,14 @@ from __future__ import annotations +import logging from typing import Any from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST -from .pyoasismini import OasisMini +from .pyoasismini import TRACKS, OasisMini + +_LOGGER = logging.getLogger(__name__) def create_client(data: dict[str, Any]) -> OasisMini: @@ -27,3 +30,21 @@ async def add_and_play_track(device: OasisMini, track: int) -> None: if device.status_code != 4: await device.async_play() + + +def get_track_id(track: str) -> int | None: + """Get a track id. + + `track` can be either an id or title + """ + track = track.lower().strip() + if track not in map(str, TRACKS): + track = next( + (id for id, info in TRACKS.items() if info["name"].lower() == track), track + ) + + try: + return int(track) + except ValueError: + _LOGGER.warning("Invalid track: %s", track) + return None diff --git a/custom_components/oasis_mini/media_player.py b/custom_components/oasis_mini/media_player.py index ba2c48f..f3a5437 100644 --- a/custom_components/oasis_mini/media_player.py +++ b/custom_components/oasis_mini/media_player.py @@ -19,7 +19,9 @@ from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import OasisMiniConfigEntry +from .const import DOMAIN from .entity import OasisMiniEntity +from .helpers import get_track_id from .pyoasismini.const import TRACKS @@ -102,18 +104,30 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity): return MediaPlayerState.ON return MediaPlayerState.IDLE + def abort_if_busy(self) -> None: + """Abort if the device is currently busy.""" + if self.device.busy: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="device_busy", + translation_placeholders={"name": self._friendly_name_internal()}, + ) + async def async_media_pause(self) -> None: """Send pause command.""" + self.abort_if_busy() await self.device.async_pause() await self.coordinator.async_request_refresh() async def async_media_play(self) -> None: """Send play command.""" + self.abort_if_busy() await self.device.async_play() await self.coordinator.async_request_refresh() async def async_media_stop(self) -> None: """Send stop command.""" + self.abort_if_busy() await self.device.async_stop() await self.coordinator.async_request_refresh() @@ -127,6 +141,7 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity): async def async_media_previous_track(self) -> None: """Send previous track command.""" + self.abort_if_busy() if (index := self.device.playlist_index - 1) < 0: index = len(self.device.playlist) - 1 await self.device.async_change_track(index) @@ -134,6 +149,7 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity): async def async_media_next_track(self) -> None: """Send next track command.""" + self.abort_if_busy() if (index := self.device.playlist_index + 1) >= len(self.device.playlist): index = 0 await self.device.async_change_track(index) @@ -147,32 +163,29 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity): **kwargs: Any, ) -> None: """Play a piece of media.""" - if media_id not in map(str, TRACKS): - media_id = next( - ( - id - for id, info in TRACKS.items() - if info["name"].lower() == media_id.lower() - ), - media_id, - ) - try: - track = int(media_id) - except ValueError as err: - raise ServiceValidationError(f"Invalid media: {media_id}") from err + self.abort_if_busy() + if media_type == MediaType.PLAYLIST: + raise ServiceValidationError("Playlists are not currently supported") + else: + track = list(filter(None, map(get_track_id, media_id.split(",")))) + if not track: + raise ServiceValidationError(f"Invalid media: {media_id}") device = self.device enqueue = MediaPlayerEnqueue.NEXT if not enqueue else enqueue if enqueue == MediaPlayerEnqueue.REPLACE: - await device.async_set_playlist([track]) + await device.async_set_playlist(track) else: await device.async_add_track_to_playlist(track) if enqueue in (MediaPlayerEnqueue.NEXT, MediaPlayerEnqueue.PLAY): # Move track to next item in the playlist - if (index := (len(device.playlist) - 1)) != device.playlist_index: + new_tracks = 1 if isinstance(track, int) else len(track) + if (index := (len(device.playlist) - new_tracks)) != device.playlist_index: if index != ( - _next := min(device.playlist_index + 1, len(device.playlist) - 1) + _next := min( + device.playlist_index + 1, len(device.playlist) - new_tracks + ) ): await device.async_move_track(index, _next) if enqueue == MediaPlayerEnqueue.PLAY: @@ -188,6 +201,7 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity): async def async_clear_playlist(self) -> None: """Clear players playlist.""" + self.abort_if_busy() await self.device.async_clear_playlist() await self.coordinator.async_request_refresh() diff --git a/custom_components/oasis_mini/pyoasismini/__init__.py b/custom_components/oasis_mini/pyoasismini/__init__.py index 248262a..0b54fbf 100644 --- a/custom_components/oasis_mini/pyoasismini/__init__.py +++ b/custom_components/oasis_mini/pyoasismini/__init__.py @@ -8,7 +8,6 @@ from typing import Any, Awaitable, Final from urllib.parse import urljoin from aiohttp import ClientResponseError, ClientSession -import async_timeout from .const import TRACKS from .utils import _bit_to_bool, decrypt_svg_content @@ -159,17 +158,17 @@ class OasisMini: """Return the url.""" return f"http://{self._host}/" - async def async_add_track_to_playlist(self, track: int) -> None: + async def async_add_track_to_playlist(self, track: int | list[int]) -> None: """Add track to playlist.""" if not track: return - + if isinstance(track, int): + track = [track] if 0 in self.playlist: - playlist = [t for t in self.playlist if t] + [track] + playlist = [t for t in self.playlist if t] + track return await self.async_set_playlist(playlist) - await self._async_command(params={"ADDJOBLIST": track}) - self.playlist.append(track) + self.playlist.extend(track) async def async_change_track(self, index: int) -> None: """Change the track.""" @@ -312,8 +311,10 @@ class OasisMini: raise ValueError("Invalid pause option specified") await self._async_command(params={"WRIWAITAFTER": option}) - async def async_set_playlist(self, playlist: list[int]) -> None: + async def async_set_playlist(self, playlist: list[int] | int) -> None: """Set the playlist.""" + if isinstance(playlist, int): + playlist = [playlist] if is_playing := (self.status_code == 4): await self.async_stop() await self._async_command(params={"WRIJOBLIST": ",".join(map(str, playlist))})