1
0
mirror of https://github.com/natekspencer/hacs-oasis_mini.git synced 2025-11-08 05:03:52 -05:00

Add cloud playlists

This commit is contained in:
Nathan Spencer
2025-08-02 13:48:58 +00:00
parent 7c650949d8
commit 8a72aba294
8 changed files with 120 additions and 18 deletions

View File

@@ -3,12 +3,14 @@
from __future__ import annotations
import logging
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
import homeassistant.helpers.device_registry as dr
import homeassistant.helpers.entity_registry as er
from .const import DOMAIN
from .coordinator import OasisMiniCoordinator
@@ -89,3 +91,33 @@ async def async_remove_entry(hass: HomeAssistant, entry: OasisMiniConfigEntry) -
async def update_listener(hass: HomeAssistant, entry: OasisMiniConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Migrate old entry."""
_LOGGER.debug(
"Migrating configuration from version %s.%s", entry.version, entry.minor_version
)
if entry.version == 1 and entry.minor_version == 1:
# Need to update previous playlist select entity to queue
@callback
def migrate_unique_id(entity_entry: er.RegistryEntry) -> dict[str, Any] | None:
"""Migrate the playlist unique ID to queue."""
if entity_entry.domain == "select" and entity_entry.unique_id.endswith(
"-playlist"
):
unique_id = entity_entry.unique_id.replace("-playlist", "-queue")
return {"new_unique_id": unique_id}
return None
await er.async_migrate_entries(hass, entry.entry_id, migrate_unique_id)
hass.config_entries.async_update_entry(entry, minor_version=2, version=1)
_LOGGER.debug(
"Migration to configuration version %s.%s successful",
entry.version,
entry.minor_version,
)
return True

View File

@@ -62,6 +62,7 @@ class OasisMiniConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Oasis Mini."""
VERSION = 1
MINOR_VERSION = 2
@staticmethod
@callback

View File

@@ -50,6 +50,7 @@ class OasisMiniCoordinator(DataUpdateCoordinator[str]):
self.attempt = 0
await self.device.async_get_current_track_details()
await self.device.async_get_playlist_details()
await self.device.async_cloud_get_playlists()
except Exception as ex: # pylint:disable=broad-except
if self.attempt > 2 or not (data or self.data):
raise UpdateFailed(

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
from datetime import datetime, timedelta
import logging
from typing import Any, Awaitable, Final
from urllib.parse import urljoin
@@ -10,7 +11,7 @@ from urllib.parse import urljoin
from aiohttp import ClientResponseError, ClientSession
from .const import TRACKS
from .utils import _bit_to_bool, _parse_int, decrypt_svg_content
from .utils import _bit_to_bool, _parse_int, decrypt_svg_content, now
_LOGGER = logging.getLogger(__name__)
@@ -89,6 +90,8 @@ BALL_SPEED_MIN: Final = 100
LED_SPEED_MAX: Final = 90
LED_SPEED_MIN: Final = -90
PLAYLISTS_REFRESH_LIMITER = timedelta(minutes=5)
class OasisMini:
"""Oasis Mini API client class."""
@@ -116,6 +119,9 @@ class OasisMini:
repeat_playlist: bool
status_code: int
playlists: list[dict[str, Any]] = []
_playlists_next_refresh: datetime = now()
def __init__(
self,
host: str,
@@ -385,6 +391,18 @@ class OasisMini:
"""Login via the cloud."""
await self._async_cloud_request("GET", "api/auth/logout")
async def async_cloud_get_playlists(
self, personal_only: bool = False
) -> list[dict[str, Any]]:
"""Get playlists from the cloud."""
if self._playlists_next_refresh <= now():
if playlists := await self._async_cloud_request(
"GET", "api/playlist", params={"my_playlists": str(personal_only)}
):
self.playlists = playlists
self._playlists_next_refresh = now() + PLAYLISTS_REFRESH_LIMITER
return self.playlists
async def async_cloud_get_track_info(self, track_id: int) -> dict[str, Any] | None:
"""Get cloud track info."""
try:
@@ -404,7 +422,7 @@ class OasisMini:
"GET", "api/track", params={"ids[]": tracks or []}
)
if not response:
return None
return []
track_details = response.get("data", [])
while next_page_url := response.get("next_page_url"):
response = await self._async_cloud_request("GET", next_page_url)

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import base64
from datetime import UTC, datetime
import logging
import math
from xml.etree.ElementTree import Element, SubElement, tostring
@@ -177,3 +178,7 @@ def decrypt_svg_content(svg_content: dict[str, str]):
svg_content["decrypted"] = decrypted
return decrypted
def now() -> datetime:
return datetime.now(UTC)

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass
from typing import Any, Awaitable, Callable
@@ -63,12 +64,33 @@ class OasisMiniSelectEntity(OasisMiniEntity, SelectEntity):
return super()._handle_coordinator_update()
def playlist_update_handler(entity: OasisMiniSelectEntity) -> None:
"""Handle playlist updates."""
def playlists_update_handler(entity: OasisMiniSelectEntity) -> None:
"""Handle playlists updates."""
# pylint: disable=protected-access
device = entity.device
options = [
device._playlist.get(track, {}).get(
counts = defaultdict(int)
options = []
current_option: str | None = None
for playlist in device.playlists:
name = playlist["name"]
counts[name] += 1
if counts[name] > 1:
name = f"{name} ({counts[name]})"
options.append(name)
if device.playlist == [pattern["id"] for pattern in playlist["patterns"]]:
current_option = name
entity._attr_options = options
entity._attr_current_option = current_option
def queue_update_handler(entity: OasisMiniSelectEntity) -> None:
"""Handle queue updates."""
# pylint: disable=protected-access
device = entity.device
counts = defaultdict(int)
options = []
for track in device.playlist:
name = device._playlist.get(track, {}).get(
"name",
TRACKS.get(track, {"id": track, "name": f"Unknown Title (#{track})"}).get(
"name",
@@ -77,8 +99,10 @@ def playlist_update_handler(entity: OasisMiniSelectEntity) -> None:
else str(track),
),
)
for track in device.playlist
]
counts[name] += 1
if counts[name] > 1:
name = f"{name} ({counts[name]})"
options.append(name)
entity._attr_options = options
index = min(device.playlist_index, len(options) - 1)
entity._attr_current_option = options[index] if options else None
@@ -93,11 +117,22 @@ DESCRIPTORS = (
select_fn=lambda device, option: device.async_set_autoplay(option),
),
OasisMiniSelectEntityDescription(
key="playlist",
translation_key="playlist",
key="queue",
translation_key="queue",
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,
update_handler=queue_update_handler,
),
)
CLOUD_DESCRIPTORS = (
OasisMiniSelectEntityDescription(
key="playlists",
translation_key="playlist",
current_value=lambda device: (device.playlists, device.playlist.copy()),
select_fn=lambda device, option: device.async_set_playlist(
[pattern["id"] for pattern in device.playlists[option]["patterns"]]
),
update_handler=playlists_update_handler,
),
)
@@ -108,9 +143,13 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Oasis Mini select using config entry."""
async_add_entities(
[
OasisMiniSelectEntity(entry.runtime_data, descriptor)
for descriptor in DESCRIPTORS
]
)
coordinator: OasisMiniCoordinator = entry.runtime_data
entities = [
OasisMiniSelectEntity(coordinator, descriptor) for descriptor in DESCRIPTORS
]
if coordinator.device.access_token:
entities.extend(
OasisMiniSelectEntity(coordinator, descriptor)
for descriptor in CLOUD_DESCRIPTORS
)
async_add_entities(entities)

View File

@@ -73,6 +73,9 @@
},
"playlist": {
"name": "Playlist"
},
"queue": {
"name": "Queue"
}
},
"sensor": {

View File

@@ -73,6 +73,9 @@
},
"playlist": {
"name": "Playlist"
},
"queue": {
"name": "Queue"
}
},
"sensor": {