mirror of
https://github.com/natekspencer/hacs-oasis_mini.git
synced 2025-12-06 18:44:14 -05:00
Swap out direct HTTP connection with server MQTT connection to handle firmware 2.60+ (#98)
* Switch to using mqtt * Better mqtt handling when connection is interrupted * Get track info from the cloud when playlist or index changes * Add additional helpers * Dynamically handle devices and other enhancements * 📝 Add docstrings to `mqtt` Docstrings generation was requested by @natekspencer. * https://github.com/natekspencer/hacs-oasis_mini/pull/98#issuecomment-3568450288 The following files were modified: * `custom_components/oasis_mini/__init__.py` * `custom_components/oasis_mini/binary_sensor.py` * `custom_components/oasis_mini/button.py` * `custom_components/oasis_mini/config_flow.py` * `custom_components/oasis_mini/coordinator.py` * `custom_components/oasis_mini/entity.py` * `custom_components/oasis_mini/helpers.py` * `custom_components/oasis_mini/image.py` * `custom_components/oasis_mini/light.py` * `custom_components/oasis_mini/media_player.py` * `custom_components/oasis_mini/number.py` * `custom_components/oasis_mini/pyoasiscontrol/clients/cloud_client.py` * `custom_components/oasis_mini/pyoasiscontrol/clients/http_client.py` * `custom_components/oasis_mini/pyoasiscontrol/clients/mqtt_client.py` * `custom_components/oasis_mini/pyoasiscontrol/clients/transport.py` * `custom_components/oasis_mini/pyoasiscontrol/device.py` * `custom_components/oasis_mini/pyoasiscontrol/utils.py` * `custom_components/oasis_mini/select.py` * `custom_components/oasis_mini/sensor.py` * `custom_components/oasis_mini/switch.py` * `custom_components/oasis_mini/update.py` * `update_tracks.py` * Fix formatting in transport.py * Replace tabs with spaces * Use tuples instead of sets for descriptors * Encode svg in image entity * Fix iot_class * Fix tracks list url * Ensure update_tracks closes the connection * Fix number typing and docstring * Fix docstring in update_tracks * Cache playlist based on type * Fix formatting in device.py * Add missing async_send_auto_clean_command to http client * Propagate UnauthenticatedError from async_get_track_info * Adjust exceptions * Move create_client outside of try block in config_flow * Formatting * Address PR comments * Formatting * Add noqa: ARG001 on unused hass * Close cloud/MQTT clients if initial coordinator refresh fails. * Address PR again * PR fixes * Pass config entry to coordinator * Remove async_timeout (thanks ChatGPT... not) * Address PR * Replace magic numbers for status code * Update autoplay wording/ordering --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Oasis Mini media player entity."""
|
||||
"""Oasis device media player entity."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -18,15 +18,53 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import OasisMiniConfigEntry
|
||||
from . import OasisDeviceConfigEntry, setup_platform_from_coordinator
|
||||
from .const import DOMAIN
|
||||
from .entity import OasisMiniEntity
|
||||
from .entity import OasisDeviceEntity
|
||||
from .helpers import get_track_id
|
||||
from .pyoasismini.const import TRACKS
|
||||
from .pyoasiscontrol import OasisDevice
|
||||
from .pyoasiscontrol.const import (
|
||||
STATUS_CENTERING,
|
||||
STATUS_DOWNLOADING,
|
||||
STATUS_ERROR,
|
||||
STATUS_LIVE,
|
||||
STATUS_PAUSED,
|
||||
STATUS_PLAYING,
|
||||
STATUS_STOPPED,
|
||||
STATUS_UPDATING,
|
||||
)
|
||||
|
||||
|
||||
class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity):
|
||||
"""Oasis Mini media player entity."""
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, # noqa: ARG001
|
||||
entry: OasisDeviceConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Oasis device media_players using config entry."""
|
||||
|
||||
def make_entities(new_devices: list[OasisDevice]):
|
||||
"""
|
||||
Create media player entities for the given Oasis devices.
|
||||
|
||||
Parameters:
|
||||
new_devices (list[OasisDevice]): Devices to wrap as media player entities.
|
||||
|
||||
Returns:
|
||||
list[OasisDeviceMediaPlayerEntity]: Media player entities corresponding to each device.
|
||||
"""
|
||||
return [
|
||||
OasisDeviceMediaPlayerEntity(entry.runtime_data, device, DESCRIPTOR)
|
||||
for device in new_devices
|
||||
]
|
||||
|
||||
setup_platform_from_coordinator(entry, async_add_entities, make_entities)
|
||||
|
||||
|
||||
DESCRIPTOR = MediaPlayerEntityDescription(key="oasis_mini", name=None)
|
||||
|
||||
|
||||
class OasisDeviceMediaPlayerEntity(OasisDeviceEntity, MediaPlayerEntity):
|
||||
"""Oasis device media player entity."""
|
||||
|
||||
_attr_media_image_remotely_accessible = True
|
||||
_attr_supported_features = (
|
||||
@@ -55,16 +93,22 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity):
|
||||
|
||||
@property
|
||||
def media_image_url(self) -> str | None:
|
||||
"""Image url of current playing media."""
|
||||
if not (track := self.device.track):
|
||||
track = TRACKS.get(self.device.track_id)
|
||||
if track and "image" in track:
|
||||
return f"https://app.grounded.so/uploads/{track['image']}"
|
||||
return None
|
||||
"""
|
||||
URL of the image representing the currently playing media.
|
||||
|
||||
Returns:
|
||||
The image URL as a string, or `None` if no image is available.
|
||||
"""
|
||||
return self.device.track_image_url
|
||||
|
||||
@property
|
||||
def media_position(self) -> int:
|
||||
"""Position of current playing media in seconds."""
|
||||
"""
|
||||
Playback position of the current media in seconds.
|
||||
|
||||
Returns:
|
||||
int: Position in seconds of the currently playing media.
|
||||
"""
|
||||
return self.device.progress
|
||||
|
||||
@property
|
||||
@@ -74,33 +118,39 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity):
|
||||
|
||||
@property
|
||||
def media_title(self) -> str | None:
|
||||
"""Title of current playing media."""
|
||||
if not self.device.track_id:
|
||||
return None
|
||||
if not (track := self.device.track):
|
||||
track = TRACKS.get(self.device.track_id, {})
|
||||
return track.get("name", f"Unknown Title (#{self.device.track_id})")
|
||||
"""
|
||||
Provide the title of the currently playing track.
|
||||
|
||||
Returns:
|
||||
str | None: The track title, or None if no title is available.
|
||||
"""
|
||||
return self.device.track_name
|
||||
|
||||
@property
|
||||
def repeat(self) -> RepeatMode:
|
||||
"""Return current repeat mode."""
|
||||
"""
|
||||
Get the current repeat mode for the device.
|
||||
|
||||
Returns:
|
||||
`RepeatMode.ALL` if the device is configured to repeat the playlist, `RepeatMode.OFF` otherwise.
|
||||
"""
|
||||
return RepeatMode.ALL if self.device.repeat_playlist else RepeatMode.OFF
|
||||
|
||||
@property
|
||||
def state(self) -> MediaPlayerState:
|
||||
"""State of the player."""
|
||||
status_code = self.device.status_code
|
||||
if self.device.error or status_code in (9, 11):
|
||||
if self.device.error or status_code in (STATUS_ERROR, STATUS_UPDATING):
|
||||
return MediaPlayerState.OFF
|
||||
if status_code == 2:
|
||||
if status_code == STATUS_STOPPED:
|
||||
return MediaPlayerState.IDLE
|
||||
if status_code in (3, 13):
|
||||
if status_code in (STATUS_CENTERING, STATUS_DOWNLOADING):
|
||||
return MediaPlayerState.BUFFERING
|
||||
if status_code == 4:
|
||||
if status_code == STATUS_PLAYING:
|
||||
return MediaPlayerState.PLAYING
|
||||
if status_code == 5:
|
||||
if status_code == STATUS_PAUSED:
|
||||
return MediaPlayerState.PAUSED
|
||||
if status_code == 15:
|
||||
if status_code == STATUS_LIVE:
|
||||
return MediaPlayerState.ON
|
||||
return MediaPlayerState.IDLE
|
||||
|
||||
@@ -114,46 +164,75 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity):
|
||||
)
|
||||
|
||||
async def async_media_pause(self) -> None:
|
||||
"""Send pause command."""
|
||||
"""
|
||||
Pause playback on the device.
|
||||
|
||||
Raises:
|
||||
ServiceValidationError: If the device is busy and cannot accept commands.
|
||||
"""
|
||||
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."""
|
||||
"""
|
||||
Start playback on the device.
|
||||
|
||||
Raises:
|
||||
ServiceValidationError: If the device is currently busy.
|
||||
"""
|
||||
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."""
|
||||
"""
|
||||
Stop playback on the Oasis device.
|
||||
|
||||
Raises:
|
||||
ServiceValidationError: If the device is currently busy.
|
||||
"""
|
||||
self.abort_if_busy()
|
||||
await self.device.async_stop()
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_set_repeat(self, repeat: RepeatMode) -> None:
|
||||
"""Set repeat mode."""
|
||||
"""
|
||||
Set the device playlist repeat behavior.
|
||||
|
||||
Enables or disables looping of the playlist according to the provided RepeatMode:
|
||||
- RepeatMode.OFF disables playlist repeat.
|
||||
- RepeatMode.ALL enables playlist repeat for the entire playlist.
|
||||
- RepeatMode.ONE enables single-track repeat, except when the device is currently repeating the entire playlist; in that case the playlist repeat is disabled to preserve single-track semantics.
|
||||
|
||||
Parameters:
|
||||
repeat (RepeatMode): The desired repeat mode to apply to the device playlist.
|
||||
"""
|
||||
await self.device.async_set_repeat_playlist(
|
||||
repeat != RepeatMode.OFF
|
||||
and not (repeat == RepeatMode.ONE and self.repeat == RepeatMode.ALL)
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send previous track command."""
|
||||
"""
|
||||
Move playback to the previous track in the device's playlist, wrapping to the last track when currently at the first.
|
||||
|
||||
Raises:
|
||||
ServiceValidationError: If the device is busy.
|
||||
"""
|
||||
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)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_media_next_track(self) -> None:
|
||||
"""Send next track command."""
|
||||
"""
|
||||
Advance the device to the next track in its playlist, wrapping to the first track when at the end.
|
||||
|
||||
Raises:
|
||||
ServiceValidationError: if the device is busy.
|
||||
"""
|
||||
self.abort_if_busy()
|
||||
if (index := self.device.playlist_index + 1) >= len(self.device.playlist):
|
||||
index = 0
|
||||
await self.device.async_change_track(index)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_play_media(
|
||||
self,
|
||||
@@ -162,7 +241,19 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity):
|
||||
enqueue: MediaPlayerEnqueue | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Play a piece of media."""
|
||||
"""
|
||||
Play or enqueue one or more Oasis tracks on the device.
|
||||
|
||||
Validates the media type and parses one or more track identifiers from `media_id`, then updates the device playlist according to `enqueue`. Depending on the enqueue mode the method can replace the playlist, append tracks, move appended tracks to the next play position, and optionally start playback.
|
||||
|
||||
Parameters:
|
||||
media_type (MediaType | str): The media type being requested.
|
||||
media_id (str): A comma-separated string of track identifiers.
|
||||
enqueue (MediaPlayerEnqueue | None): How to insert the tracks into the playlist; if omitted defaults to NEXT.
|
||||
|
||||
Raises:
|
||||
ServiceValidationError: If the device is busy, if `media_type` is a playlist (playlists are unsupported), or if `media_id` does not contain any valid track identifiers.
|
||||
"""
|
||||
self.abort_if_busy()
|
||||
if media_type == MediaType.PLAYLIST:
|
||||
raise ServiceValidationError(
|
||||
@@ -203,22 +294,12 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity):
|
||||
):
|
||||
await device.async_play()
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_clear_playlist(self) -> None:
|
||||
"""Clear players playlist."""
|
||||
"""
|
||||
Clear the device's playlist.
|
||||
|
||||
Raises:
|
||||
ServiceValidationError: If the device is busy and cannot accept commands.
|
||||
"""
|
||||
self.abort_if_busy()
|
||||
await self.device.async_clear_playlist()
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
|
||||
DESCRIPTOR = MediaPlayerEntityDescription(key="oasis_mini", name=None)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: OasisMiniConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Oasis Mini media_players using config entry."""
|
||||
async_add_entities([OasisMiniMediaPlayerEntity(entry.runtime_data, DESCRIPTOR)])
|
||||
|
||||
Reference in New Issue
Block a user