1
0
mirror of https://github.com/natekspencer/hacs-oasis_mini.git synced 2025-11-12 23:23:51 -05:00

Add support for enqueue options in media_player.play_media service and other minor improvements

This commit is contained in:
Nathan Spencer
2024-07-31 19:16:15 -06:00
parent d70dd0a650
commit cc80c295f6
8 changed files with 119 additions and 14 deletions

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription
from homeassistant.config_entries import ConfigEntry 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 homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN
@@ -42,6 +42,7 @@ class OasisMiniImageEntity(OasisMiniEntity, ImageEntity):
) )
return self._cached_image.content return self._cached_image.content
@callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator.""" """Handle updated data from the coordinator."""
if self._track_id != self.device.track_id or ( if self._track_id != self.device.track_id or (

View File

@@ -7,6 +7,7 @@ import logging
from typing import Any from typing import Any
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
MediaPlayerEnqueue,
MediaPlayerEntity, MediaPlayerEntity,
MediaPlayerEntityDescription, MediaPlayerEntityDescription,
MediaPlayerEntityFeature, MediaPlayerEntityFeature,
@@ -39,6 +40,7 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity):
| MediaPlayerEntityFeature.PREVIOUS_TRACK | MediaPlayerEntityFeature.PREVIOUS_TRACK
| MediaPlayerEntityFeature.NEXT_TRACK | MediaPlayerEntityFeature.NEXT_TRACK
| MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.PLAY_MEDIA
| MediaPlayerEntityFeature.MEDIA_ENQUEUE
| MediaPlayerEntityFeature.CLEAR_PLAYLIST | MediaPlayerEntityFeature.CLEAR_PLAYLIST
| MediaPlayerEntityFeature.REPEAT_SET | MediaPlayerEntityFeature.REPEAT_SET
) )
@@ -144,7 +146,11 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity):
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()
async def async_play_media( 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: ) -> None:
"""Play a piece of media.""" """Play a piece of media."""
if media_id not in TRACKS: if media_id not in TRACKS:
@@ -157,15 +163,33 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity):
media_id, media_id,
) )
try: try:
media_id = int(media_id) track = int(media_id)
except ValueError as err: except ValueError as err:
raise ServiceValidationError(f"Invalid media: {media_id}") from 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: async def async_clear_playlist(self) -> None:
"""Clear players playlist.""" """Clear players playlist."""
await self.device.async_set_playlist([0]) await self.device.async_clear_playlist()
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()

View File

@@ -5,7 +5,8 @@ import logging
from typing import Any, Awaitable, Callable, Final from typing import Any, Awaitable, Callable, Final
from urllib.parse import urljoin from urllib.parse import urljoin
from aiohttp import ClientSession from aiohttp import ClientResponseError, ClientSession
import async_timeout
from .utils import _bit_to_bool from .utils import _bit_to_bool
@@ -15,7 +16,7 @@ STATUS_CODE_MAP = {
0: "booting", # maybe? 0: "booting", # maybe?
2: "stopped", 2: "stopped",
3: "centering", 3: "centering",
4: "running", 4: "playing",
5: "paused", 5: "paused",
9: "error", 9: "error",
11: "updating", 11: "updating",
@@ -90,6 +91,7 @@ class OasisMini:
autoplay: str autoplay: str
brightness: int brightness: int
busy: bool
color: str color: str
download_progress: int download_progress: int
error: int error: int
@@ -175,7 +177,7 @@ class OasisMini:
async def async_add_track_to_playlist(self, track: int) -> None: async def async_add_track_to_playlist(self, track: int) -> None:
"""Add track to playlist.""" """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] playlist = [t for t in self.playlist if t] + [track]
await self.async_set_playlist(playlist) await self.async_set_playlist(playlist)
else: else:
@@ -188,6 +190,10 @@ class OasisMini:
raise ValueError("Invalid index specified") raise ValueError("Invalid index specified")
await self._async_command(params={"CMDCHANGETRACK": index}) 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: async def async_get_ip_address(self) -> str | None:
"""Get the ip address.""" """Get the ip address."""
self._ip_address = await self._async_get(params={"GETIP": ""}) self._ip_address = await self._async_get(params={"GETIP": ""})
@@ -235,7 +241,8 @@ class OasisMini:
"""Send play command.""" """Send play command."""
if self.status_code == 15: if self.status_code == 15:
await self.async_stop() 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: async def async_reboot(self) -> None:
"""Send reboot command.""" """Send reboot command."""
@@ -294,9 +301,13 @@ class OasisMini:
await self._async_command(params={"WRIWAITAFTER": option}) 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]) -> 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))}) await self._async_command(params={"WRIJOBLIST": ",".join(map(str, playlist))})
self.playlist = playlist self.playlist = playlist
if is_playing:
await self.async_play()
async def async_set_repeat_playlist(self, repeat: bool) -> None: async def async_set_repeat_playlist(self, repeat: bool) -> None:
"""Set repeat playlist.""" """Set repeat playlist."""
@@ -327,6 +338,9 @@ class OasisMini:
"""Get cloud track info.""" """Get cloud track info."""
try: try:
return await self._async_cloud_request("GET", f"api/track/{track_id}") 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: except Exception as ex:
_LOGGER.exception(ex) _LOGGER.exception(ex)
return None return None
@@ -386,8 +400,11 @@ class OasisMini:
async def _async_command(self, **kwargs: Any) -> str | None: async def _async_command(self, **kwargs: Any) -> str | None:
"""Send a command to the device.""" """Send a command to the device."""
result = await self._async_get(**kwargs) with async_timeout.timeout(5):
_LOGGER.debug("Result: %s", result) 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: async def _async_get(self, **kwargs: Any) -> str | None:
"""Perform a GET request.""" """Perform a GET request."""

View File

@@ -918,5 +918,30 @@
"name": "Yin yang", "name": "Yin yang",
"author": "001547.d33e09ec63fb4259a31a494ad194e028.0314", "author": "001547.d33e09ec63fb4259a31a494ad194e028.0314",
"image": "2024/07/36fe669628c5e4dfd6d33a263196a750.svg" "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"
} }
} }

View File

@@ -7,7 +7,7 @@ from typing import Any, Awaitable, Callable
from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry 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 import EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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.entity_description.select_fn(self.device, self.options.index(option))
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()
@callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator.""" """Handle updated data from the coordinator."""
new_value = self.entity_description.current_value(self.device) new_value = self.entity_description.current_value(self.device)
@@ -89,7 +90,7 @@ DESCRIPTORS = (
OasisMiniSelectEntityDescription( OasisMiniSelectEntityDescription(
key="playlist", key="playlist",
name="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), select_fn=lambda device, option: device.async_change_track(option),
update_handler=playlist_update_handler, update_handler=playlist_update_handler,
), ),

View File

@@ -49,6 +49,7 @@ DESCRIPTORS = {
SensorEntityDescription( SensorEntityDescription(
key=key, key=key,
name=key.replace("_", " ").capitalize(), name=key.replace("_", " ").capitalize(),
translation_key=key,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
) )

View File

@@ -36,5 +36,23 @@
"error": { "error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" "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"
}
}
}
} }
} }

View File

@@ -36,5 +36,23 @@
"error": { "error": {
"invalid_auth": "Invalid authentication" "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"
}
}
}
} }
} }