diff --git a/custom_components/oasis_mini/image.py b/custom_components/oasis_mini/image.py index 0279af6..8a53a03 100644 --- a/custom_components/oasis_mini/image.py +++ b/custom_components/oasis_mini/image.py @@ -4,7 +4,7 @@ from __future__ import annotations from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -42,6 +42,7 @@ class OasisMiniImageEntity(OasisMiniEntity, ImageEntity): ) return self._cached_image.content + @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" if self._track_id != self.device.track_id or ( diff --git a/custom_components/oasis_mini/media_player.py b/custom_components/oasis_mini/media_player.py index 9dcbaf5..f1e1eba 100644 --- a/custom_components/oasis_mini/media_player.py +++ b/custom_components/oasis_mini/media_player.py @@ -7,6 +7,7 @@ import logging from typing import Any from homeassistant.components.media_player import ( + MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityDescription, MediaPlayerEntityFeature, @@ -39,6 +40,7 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity): | MediaPlayerEntityFeature.PREVIOUS_TRACK | MediaPlayerEntityFeature.NEXT_TRACK | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.MEDIA_ENQUEUE | MediaPlayerEntityFeature.CLEAR_PLAYLIST | MediaPlayerEntityFeature.REPEAT_SET ) @@ -144,7 +146,11 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity): await self.coordinator.async_request_refresh() async def async_play_media( - self, media_type: MediaType | str, media_id: str, **kwargs: Any + self, + media_type: MediaType | str, + media_id: str, + enqueue: MediaPlayerEnqueue | None = None, + **kwargs: Any, ) -> None: """Play a piece of media.""" if media_id not in TRACKS: @@ -157,15 +163,33 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity): media_id, ) try: - media_id = int(media_id) + track = int(media_id) except ValueError as err: raise ServiceValidationError(f"Invalid media: {media_id}") from err - await add_and_play_track(self.device, media_id) + device = self.device + enqueue = MediaPlayerEnqueue.NEXT if not enqueue else enqueue + if enqueue == MediaPlayerEnqueue.REPLACE: + 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 (idx := (len(device.playlist) - 1)) != device.playlist_index: + if idx != (nxt := min(device.playlist_index + 1, len(device.playlist))): + await device.async_move_track(idx, nxt) + if enqueue == MediaPlayerEnqueue.PLAY: + await device.async_change_track(nxt) + + if device.status_code != 4: + await device.async_play() + + await self.coordinator.async_request_refresh() async def async_clear_playlist(self) -> None: """Clear players playlist.""" - await self.device.async_set_playlist([0]) + 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 36ac00d..2172420 100644 --- a/custom_components/oasis_mini/pyoasismini/__init__.py +++ b/custom_components/oasis_mini/pyoasismini/__init__.py @@ -5,7 +5,8 @@ import logging from typing import Any, Awaitable, Callable, Final from urllib.parse import urljoin -from aiohttp import ClientSession +from aiohttp import ClientResponseError, ClientSession +import async_timeout from .utils import _bit_to_bool @@ -15,7 +16,7 @@ STATUS_CODE_MAP = { 0: "booting", # maybe? 2: "stopped", 3: "centering", - 4: "running", + 4: "playing", 5: "paused", 9: "error", 11: "updating", @@ -90,6 +91,7 @@ class OasisMini: autoplay: str brightness: int + busy: bool color: str download_progress: int error: int @@ -175,7 +177,7 @@ class OasisMini: async def async_add_track_to_playlist(self, track: int) -> None: """Add track to playlist.""" - if 0 in self.playlist: + if track and 0 in self.playlist: playlist = [t for t in self.playlist if t] + [track] await self.async_set_playlist(playlist) else: @@ -188,6 +190,10 @@ class OasisMini: raise ValueError("Invalid index specified") await self._async_command(params={"CMDCHANGETRACK": index}) + async def async_clear_playlist(self) -> None: + """Clear the playlist.""" + await self.async_set_playlist([0]) + async def async_get_ip_address(self) -> str | None: """Get the ip address.""" self._ip_address = await self._async_get(params={"GETIP": ""}) @@ -235,7 +241,8 @@ class OasisMini: """Send play command.""" if self.status_code == 15: await self.async_stop() - await self._async_command(params={"CMDPLAY": ""}) + if self.track_id: + await self._async_command(params={"CMDPLAY": ""}) async def async_reboot(self) -> None: """Send reboot command.""" @@ -294,9 +301,13 @@ class OasisMini: await self._async_command(params={"WRIWAITAFTER": option}) async def async_set_playlist(self, playlist: list[int]) -> None: - """Set playlist.""" + """Set the playlist.""" + if is_playing := (self.status_code == 4): + await self.async_stop() await self._async_command(params={"WRIJOBLIST": ",".join(map(str, playlist))}) self.playlist = playlist + if is_playing: + await self.async_play() async def async_set_repeat_playlist(self, repeat: bool) -> None: """Set repeat playlist.""" @@ -327,6 +338,9 @@ class OasisMini: """Get cloud track info.""" try: return await self._async_cloud_request("GET", f"api/track/{track_id}") + except ClientResponseError as err: + if err.status == 404: + return {"id": track_id, "name": f"Unknown Title (#{track_id})"} except Exception as ex: _LOGGER.exception(ex) return None @@ -386,8 +400,11 @@ class OasisMini: async def _async_command(self, **kwargs: Any) -> str | None: """Send a command to the device.""" - result = await self._async_get(**kwargs) - _LOGGER.debug("Result: %s", result) + with async_timeout.timeout(5): + while self.busy: + await asyncio.sleep(0.1) + result = await self._async_get(**kwargs) + _LOGGER.debug("Result: %s", result) async def _async_get(self, **kwargs: Any) -> str | None: """Perform a GET request.""" diff --git a/custom_components/oasis_mini/pyoasismini/tracks.json b/custom_components/oasis_mini/pyoasismini/tracks.json index 5651cdc..afa8870 100644 --- a/custom_components/oasis_mini/pyoasismini/tracks.json +++ b/custom_components/oasis_mini/pyoasismini/tracks.json @@ -918,5 +918,30 @@ "name": "Yin yang", "author": "001547.d33e09ec63fb4259a31a494ad194e028.0314", "image": "2024/07/36fe669628c5e4dfd6d33a263196a750.svg" + }, + "611": { + "name": "Flow Snake", + "author": "Matt Flood", + "image": "2024/07/c5bf122bc1c8f7ef44caff6a0856bd22.svg" + }, + "576": { + "name": "Flower Wipe", + "author": "Shannon Miller", + "image": "2024/07/27205730092d3b5c866bd53b9d26be97.svg" + }, + "711": { + "name": "Looping Fractal Spirograph", + "author": "Daniel Moton", + "image": "2024/07/deb3f3c00b2cb65243099f41e00b0af5.svg" + }, + "613": { + "name": "Reverse", + "author": "Kari Hobbs", + "image": "2024/07/6c7ca5f1b8f94cf650b1793b1c2c81bb.svg" + }, + "555": { + "name": "Wavy dude", + "author": "Codie Johnston", + "image": "2024/07/8d252eed81664c520381cc21c8c3d86d.svg" } } \ No newline at end of file diff --git a/custom_components/oasis_mini/select.py b/custom_components/oasis_mini/select.py index 64e4563..84f3023 100644 --- a/custom_components/oasis_mini/select.py +++ b/custom_components/oasis_mini/select.py @@ -7,7 +7,7 @@ from typing import Any, Awaitable, Callable from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -48,6 +48,7 @@ class OasisMiniSelectEntity(OasisMiniEntity, SelectEntity): await self.entity_description.select_fn(self.device, self.options.index(option)) await self.coordinator.async_request_refresh() + @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" new_value = self.entity_description.current_value(self.device) @@ -89,7 +90,7 @@ DESCRIPTORS = ( OasisMiniSelectEntityDescription( key="playlist", name="Playlist", - current_value=lambda device: (device.playlist, device.playlist_index), + current_value=lambda device: (device.playlist.copy(), device.playlist_index), select_fn=lambda device, option: device.async_change_track(option), update_handler=playlist_update_handler, ), diff --git a/custom_components/oasis_mini/sensor.py b/custom_components/oasis_mini/sensor.py index 83c172e..0fa2361 100644 --- a/custom_components/oasis_mini/sensor.py +++ b/custom_components/oasis_mini/sensor.py @@ -49,6 +49,7 @@ DESCRIPTORS = { SensorEntityDescription( key=key, name=key.replace("_", " ").capitalize(), + translation_key=key, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ) diff --git a/custom_components/oasis_mini/strings.json b/custom_components/oasis_mini/strings.json index b9466f0..3ac2fc7 100755 --- a/custom_components/oasis_mini/strings.json +++ b/custom_components/oasis_mini/strings.json @@ -36,5 +36,23 @@ "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" } + }, + "entity": { + "sensor": { + "status": { + "name": "Status", + "state": { + "booting": "Booting", + "stopped": "Stopped", + "centering": "Centering", + "playing": "Playing", + "paused": "Paused", + "error": "Error", + "updating": "Updating", + "downloading": "Downloading", + "live drawing": "Live drawing" + } + } + } } } diff --git a/custom_components/oasis_mini/translations/en.json b/custom_components/oasis_mini/translations/en.json index 12d5dbd..ce07e01 100755 --- a/custom_components/oasis_mini/translations/en.json +++ b/custom_components/oasis_mini/translations/en.json @@ -36,5 +36,23 @@ "error": { "invalid_auth": "Invalid authentication" } + }, + "entity": { + "sensor": { + "status": { + "name": "Status", + "state": { + "booting": "Booting", + "stopped": "Stopped", + "centering": "Centering", + "playing": "Playing", + "paused": "Paused", + "error": "Error", + "updating": "Updating", + "downloading": "Downloading", + "live drawing": "Live drawing" + } + } + } } }