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

14 Commits
0.6.0 ... 0.7.4

Author SHA1 Message Date
Nathan Spencer
3014f0f11c Merge pull request #16 from natekspencer/dev
Handle invalid index bug in play random track button
2024-08-02 12:03:07 -06:00
Nathan Spencer
a44c035828 Handle invalid index bug in play random track button 2024-08-02 12:01:27 -06:00
Nathan Spencer
31276048dc Merge pull request #15 from natekspencer/natekspencer-patch-1
Create dependabot.yml
2024-08-02 07:24:40 -06:00
Nathan Spencer
742fc26a4f Create dependabot.yml 2024-08-02 07:21:26 -06:00
Nathan Spencer
3acd45da9d Merge pull request #14 from natekspencer/dev
Revert command timeout logic
2024-07-31 21:04:57 -06:00
Nathan Spencer
a736c72c8e Revert timeout changes, I'll fix later 2024-07-31 21:03:33 -06:00
Nathan Spencer
c87bb241ef Allow reboot command even if device is busy 2024-07-31 20:55:37 -06:00
Nathan Spencer
6ee81db9d4 Merge pull request #13 from natekspencer/dev
Add support for enqueue options in media_player.play_media service and other minor improvements
2024-07-31 19:28:56 -06:00
Nathan Spencer
6d6b7929d5 Fix hassfest error 2024-07-31 19:25:02 -06:00
Nathan Spencer
cc80c295f6 Add support for enqueue options in media_player.play_media service and other minor improvements 2024-07-31 19:16:15 -06:00
Nathan Spencer
423e7eba9f Merge pull request #12 from natekspencer/dev
Handle unknown track ids
2024-07-31 00:10:58 -06:00
Nathan Spencer
d70dd0a650 Handle unknown track ids 2024-07-31 00:09:21 -06:00
Nathan Spencer
cee752b6ce Merge pull request #11 from natekspencer/dev
Add additional features
2024-07-30 23:50:08 -06:00
Nathan Spencer
3b90603bef Add additional features 2024-07-30 23:47:14 -06:00
14 changed files with 357 additions and 52 deletions

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

View File

@@ -1,6 +1,9 @@
![Release](https://img.shields.io/github/v/release/natekspencer/hacs-oasis_mini?style=for-the-badge) [![Release](https://img.shields.io/github/v/release/natekspencer/hacs-oasis_mini?style=for-the-badge)](https://github.com/natekspencer/hacs-oasis_mini/releases)
[![Buy Me A Coffee/Beer](https://img.shields.io/badge/Buy_Me_A_☕/🍺-F16061?style=for-the-badge&logo=ko-fi&logoColor=white&labelColor=grey)](https://ko-fi.com/natekspencer) [![Buy Me A Coffee/Beer](https://img.shields.io/badge/Buy_Me_A_☕/🍺-F16061?style=for-the-badge&logo=ko-fi&logoColor=white&labelColor=grey)](https://ko-fi.com/natekspencer)
[![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg?style=for-the-badge)](https://github.com/hacs/integration) [![HACS Custom](https://img.shields.io/badge/HACS-Custom-41BDF5.svg?style=for-the-badge)](https://github.com/hacs/integration)
![Downloads](https://img.shields.io/github/downloads/natekspencer/hacs-oasis_mini/total?style=flat-square)
![Latest Downloads](https://img.shields.io/github/downloads/natekspencer/hacs-oasis_mini/latest/total?style=flat-square)
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://brands.home-assistant.io/oasis_mini/dark_logo.png"> <source media="(prefers-color-scheme: dark)" srcset="https://brands.home-assistant.io/oasis_mini/dark_logo.png">

View File

@@ -19,6 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN
from .coordinator import OasisMiniCoordinator from .coordinator import OasisMiniCoordinator
from .entity import OasisMiniEntity from .entity import OasisMiniEntity
from .helpers import add_and_play_track
from .pyoasismini import OasisMini from .pyoasismini import OasisMini
from .pyoasismini.const import TRACKS from .pyoasismini.const import TRACKS
@@ -39,17 +40,7 @@ async def async_setup_entry(
async def play_random_track(device: OasisMini) -> None: async def play_random_track(device: OasisMini) -> None:
"""Play random track.""" """Play random track."""
track = int(random.choice(list(TRACKS))) track = int(random.choice(list(TRACKS)))
if track not in device.playlist: await add_and_play_track(device, track)
await device.async_add_track_to_playlist(track)
# Move track to next item in the playlist and then select it
if (index := device.playlist.index(track)) != device.playlist_index:
if index != (next_index := device.playlist_index + 1):
await device.async_move_track(index, next_index)
await device.async_change_track(next_index)
if device.status_code != 4:
await device.async_play()
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)

View File

@@ -25,7 +25,11 @@ class OasisMiniCoordinator(DataUpdateCoordinator[str]):
def __init__(self, hass: HomeAssistant, device: OasisMini) -> None: def __init__(self, hass: HomeAssistant, device: OasisMini) -> None:
"""Initialize.""" """Initialize."""
super().__init__( super().__init__(
hass, _LOGGER, name=DOMAIN, update_interval=timedelta(seconds=10) hass,
_LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=10),
always_update=False,
) )
self.device = device self.device = device
@@ -44,6 +48,7 @@ class OasisMiniCoordinator(DataUpdateCoordinator[str]):
await self.device.async_get_software_version() await self.device.async_get_software_version()
data = await self.device.async_get_status() data = await self.device.async_get_status()
await self.device.async_get_current_track_details() await self.device.async_get_current_track_details()
await self.device.async_get_playlist_details()
except Exception as ex: # pylint:disable=broad-except except Exception as ex: # pylint:disable=broad-except
if self.attempt > 2 or not self.data: if self.attempt > 2 or not self.data:
raise UpdateFailed( raise UpdateFailed(

View File

@@ -12,3 +12,18 @@ from .pyoasismini import OasisMini
def create_client(data: dict[str, Any]) -> OasisMini: def create_client(data: dict[str, Any]) -> OasisMini:
"""Create a Oasis Mini local client.""" """Create a Oasis Mini local client."""
return OasisMini(data[CONF_HOST], data.get(CONF_ACCESS_TOKEN)) return OasisMini(data[CONF_HOST], data.get(CONF_ACCESS_TOKEN))
async def add_and_play_track(device: OasisMini, track: int) -> None:
"""Add and play a track."""
if track not in device.playlist:
await device.async_add_track_to_playlist(track)
# Move track to next item in the playlist and then select it
if (index := device.playlist.index(track)) != device.playlist_index:
if index != (_next := min(device.playlist_index + 1, len(device.playlist) - 1)):
await device.async_move_track(index, _next)
await device.async_change_track(_next)
if device.status_code != 4:
await device.async_play()

View File

@@ -2,16 +2,15 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription
from homeassistant.components.image import 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
from .coordinator import OasisMiniCoordinator from .coordinator import OasisMiniCoordinator
from .entity import OasisMiniEntity from .entity import OasisMiniEntity
from .pyoasismini.const import TRACKS
from .pyoasismini.utils import draw_svg from .pyoasismini.utils import draw_svg
IMAGE = ImageEntityDescription(key="image", name=None) IMAGE = ImageEntityDescription(key="image", name=None)
@@ -21,6 +20,8 @@ class OasisMiniImageEntity(OasisMiniEntity, ImageEntity):
"""Oasis Mini image entity.""" """Oasis Mini image entity."""
_attr_content_type = "image/svg+xml" _attr_content_type = "image/svg+xml"
_track_id: int | None = None
_progress: int = 0
def __init__( def __init__(
self, self,
@@ -31,15 +32,35 @@ class OasisMiniImageEntity(OasisMiniEntity, ImageEntity):
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(coordinator, entry_id, description) super().__init__(coordinator, entry_id, description)
ImageEntity.__init__(self, coordinator.hass) ImageEntity.__init__(self, coordinator.hass)
self._handle_coordinator_update()
@property
def image_last_updated(self) -> datetime | None:
"""The time when the image was last updated."""
return self.coordinator.last_updated
def image(self) -> bytes | None: def image(self) -> bytes | None:
"""Return bytes of image.""" """Return bytes of image."""
return draw_svg(self.device.track, self.device.progress, "1") if not self._cached_image:
self._cached_image = Image(
self.content_type, draw_svg(self.device.track, self._progress, "1")
)
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 (
self._progress != self.device.progress and self.device.access_token
):
self._attr_image_last_updated = self.coordinator.last_updated
self._track_id = self.device.track_id
self._progress = self.device.progress
self._cached_image = None
if not self.device.access_token:
self._attr_image_url = (
f"https://app.grounded.so/uploads/{track['image']}"
if (track := TRACKS.get(str(self.device.track_id)))
and "image" in track
else None
)
if self.hass:
super()._handle_coordinator_update()
async def async_setup_entry( async def async_setup_entry(
@@ -47,5 +68,4 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up Oasis Mini camera using config entry.""" """Set up Oasis Mini camera using config entry."""
coordinator: OasisMiniCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator: OasisMiniCoordinator = hass.data[DOMAIN][entry.entry_id]
if coordinator.device.access_token: async_add_entities([OasisMiniImageEntity(coordinator, entry, IMAGE)])
async_add_entities([OasisMiniImageEntity(coordinator, entry, IMAGE)])

View File

@@ -3,9 +3,11 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import datetime
import math import logging
from typing import Any
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
MediaPlayerEnqueue,
MediaPlayerEntity, MediaPlayerEntity,
MediaPlayerEntityDescription, MediaPlayerEntityDescription,
MediaPlayerEntityFeature, MediaPlayerEntityFeature,
@@ -15,13 +17,17 @@ from homeassistant.components.media_player import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN
from .coordinator import OasisMiniCoordinator from .coordinator import OasisMiniCoordinator
from .entity import OasisMiniEntity from .entity import OasisMiniEntity
from .helpers import add_and_play_track
from .pyoasismini.const import TRACKS from .pyoasismini.const import TRACKS
_LOGGER = logging.getLogger(__name__)
class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity): class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity):
"""Oasis Mini media player entity.""" """Oasis Mini media player entity."""
@@ -33,6 +39,8 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity):
| MediaPlayerEntityFeature.STOP | MediaPlayerEntityFeature.STOP
| MediaPlayerEntityFeature.PREVIOUS_TRACK | MediaPlayerEntityFeature.PREVIOUS_TRACK
| MediaPlayerEntityFeature.NEXT_TRACK | MediaPlayerEntityFeature.NEXT_TRACK
| MediaPlayerEntityFeature.PLAY_MEDIA
| MediaPlayerEntityFeature.MEDIA_ENQUEUE
| MediaPlayerEntityFeature.CLEAR_PLAYLIST | MediaPlayerEntityFeature.CLEAR_PLAYLIST
| MediaPlayerEntityFeature.REPEAT_SET | MediaPlayerEntityFeature.REPEAT_SET
) )
@@ -69,8 +77,10 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity):
return self.coordinator.last_updated return self.coordinator.last_updated
@property @property
def media_title(self) -> str: def media_title(self) -> str | None:
"""Title of current playing media.""" """Title of current playing media."""
if not self.device.track_id:
return None
if not (track := self.device.track): if not (track := self.device.track):
track = TRACKS.get(str(self.device.track_id), {}) track = TRACKS.get(str(self.device.track_id), {})
return track.get("name", f"Unknown Title (#{self.device.track_id})") return track.get("name", f"Unknown Title (#{self.device.track_id})")
@@ -135,9 +145,51 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity):
await self.device.async_change_track(index) await self.device.async_change_track(index)
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()
async def async_play_media(
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:
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
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,7 @@ 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
from .utils import _bit_to_bool from .utils import _bit_to_bool
@@ -15,12 +15,12 @@ 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",
13: "downloading", 13: "downloading",
15: "live drawing", 15: "live",
} }
AUTOPLAY_MAP = { AUTOPLAY_MAP = {
@@ -83,12 +83,14 @@ class OasisMini:
_access_token: str | None = None _access_token: str | None = None
_mac_address: str | None = None _mac_address: str | None = None
_ip_address: str | None = None _ip_address: str | None = None
_playlist: dict[int, dict[str, str]] = {}
_serial_number: str | None = None _serial_number: str | None = None
_software_version: str | None = None _software_version: str | None = None
_track: dict | None = None _track: dict | None = None
autoplay: str autoplay: str
brightness: int brightness: int
busy: bool
color: str color: str
download_progress: int download_progress: int
error: int error: int
@@ -174,12 +176,16 @@ 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 not track:
return
if 0 in self.playlist: 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]
await self.async_set_playlist(playlist) return await self.async_set_playlist(playlist)
else:
await self._async_command(params={"ADDJOBLIST": track}) _LOGGER.debug("Adding track %s to playlist", track)
self.playlist.append(track) await self._async_command(params={"ADDJOBLIST": track})
self.playlist.append(track)
async def async_change_track(self, index: int) -> None: async def async_change_track(self, index: int) -> None:
"""Change the track.""" """Change the track."""
@@ -187,6 +193,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([])
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": ""})
@@ -234,7 +244,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."""
@@ -293,9 +304,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."""
@@ -322,21 +337,35 @@ class OasisMini:
"""Login via the cloud.""" """Login via the cloud."""
await self._async_cloud_request("GET", "api/auth/logout") await self._async_cloud_request("GET", "api/auth/logout")
async def async_cloud_get_track_info(self, track_id: int) -> dict[str, Any]: async def async_cloud_get_track_info(self, track_id: int) -> dict[str, Any] | None:
"""Get cloud track info.""" """Get cloud track info."""
return await self._async_cloud_request("GET", f"api/track/{track_id}") 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
async def async_cloud_get_tracks(self, tracks: list[int]) -> dict: async def async_cloud_get_tracks(
self, tracks: list[int] | None = None
) -> list[dict[str, Any]]:
"""Get tracks info from the cloud""" """Get tracks info from the cloud"""
return await self._async_cloud_request( response = await self._async_cloud_request(
"GET", "api/track", params={"ids[]": tracks} "GET", "api/track", params={"ids[]": tracks or []}
) )
track_details = response.get("data", [])
while next_page_url := response.get("next_page_url"):
response = await self._async_cloud_request("GET", next_page_url)
track_details += response.get("data", [])
return track_details
async def async_cloud_get_latest_software_details(self) -> dict[str, int | str]: async def async_cloud_get_latest_software_details(self) -> dict[str, int | str]:
"""Get the latest software details from the cloud.""" """Get the latest software details from the cloud."""
return await self._async_cloud_request("GET", "api/software/last-version") return await self._async_cloud_request("GET", "api/software/last-version")
async def async_get_current_track_details(self) -> dict: async def async_get_current_track_details(self) -> dict | None:
"""Get current track info, refreshing if needed.""" """Get current track info, refreshing if needed."""
if (track := self._track) and track.get("id") == self.track_id: if (track := self._track) and track.get("id") == self.track_id:
return track return track
@@ -344,9 +373,21 @@ class OasisMini:
self._track = await self.async_cloud_get_track_info(self.track_id) self._track = await self.async_cloud_get_track_info(self.track_id)
return self._track return self._track
async def async_get_playlist_details(self) -> dict: async def async_get_playlist_details(self) -> dict[int, dict[str, str]]:
"""Get playlist info.""" """Get playlist info."""
return await self.async_cloud_get_tracks(self.playlist) if set(self.playlist).difference(self._playlist.keys()):
tracks = await self.async_cloud_get_tracks(self.playlist)
self._playlist = {
track["id"]: {
"name": track["name"],
"author": ((track.get("author") or {}).get("person") or {}).get(
"name", "Oasis Mini"
),
"image": track["image"],
}
for track in tracks
}
return self._playlist
async def _async_cloud_request(self, method: str, url: str, **kwargs: Any) -> Any: async def _async_cloud_request(self, method: str, url: str, **kwargs: Any) -> Any:
"""Perform a cloud request.""" """Perform a cloud request."""

View File

@@ -833,5 +833,115 @@
"name": "Horse", "name": "Horse",
"author": "Otávio Bittencourt", "author": "Otávio Bittencourt",
"image": "2024/07/9fec8716ce98fdbf0c02db14b47b0d66.svg" "image": "2024/07/9fec8716ce98fdbf0c02db14b47b0d66.svg"
},
"513": {
"name": "Clover Flower",
"author": "Riley P",
"image": "2024/06/b7de1c0518e5ce9cbdd8f3dd6d995e3a.svg"
},
"537": {
"name": "Full moon",
"author": "001547.d33e09ec63fb4259a31a494ad194e028.0314",
"image": "2024/07/3b06cb1bd961c01bd2411515549d907e.svg"
},
"531": {
"name": "Ghost",
"author": "Stephen Murphy",
"image": "2024/07/106d0bed641489cc5b2ee371dcfdebfa.svg"
},
"509": {
"name": "Heart loop",
"author": "000653.17c9b352828247bd858234a2a114f79b.1358",
"image": "2024/06/985c1c16fe0ce704b17229a8c7e795f5.svg"
},
"535": {
"name": "Hubcap",
"author": "001547.d33e09ec63fb4259a31a494ad194e028.0314",
"image": "2024/07/565216e030c9fa2a474c4f57366a5cc3.svg"
},
"538": {
"name": "Noise cell",
"author": "001547.d33e09ec63fb4259a31a494ad194e028.0314",
"image": "2024/07/b60bebf49043ef7969a722d826e88bf5.svg"
},
"559": {
"name": "Polymath",
"author": "Codie Johnston",
"image": "2024/07/7a5fd9826476071567967fc17ec6cb12.svg"
},
"1264": {
"name": "Raccoon",
"author": "Otávio Bittencourt",
"image": "2024/07/5ff0bd18649b029e16ac32f3b96f9715.svg"
},
"551": {
"name": "snowflake",
"author": "christina",
"image": "2024/07/77ae32407d7d2563110b1ee4607f6b7e.svg"
},
"553": {
"name": "spheres",
"author": "max",
"image": "2024/07/7a292d6cb204fbe4fc4a56ef8e4e9228.svg"
},
"544": {
"name": "Star round",
"author": "Codie Johnston",
"image": "2024/07/5e05532a764a109b090abc06f217f62e.svg"
},
"517": {
"name": "Starburst",
"author": "001547.d33e09ec63fb4259a31a494ad194e028.0314",
"image": "2024/06/ec2c03a42db75c33ddf677c6ac52e7b3.svg"
},
"1137": {
"name": "Tiger",
"author": "Otávio Bittencourt",
"image": "2024/07/02da0d000c200fb8cab3f1d38a90e077.svg"
},
"528": {
"name": "Tight spiral in to out",
"author": "Codie Johnston",
"image": "2024/06/82360f9b4c9dc169bceb99a1b4a3a13c.svg"
},
"527": {
"name": "Tight spiral out to in",
"author": "Codie Johnston",
"image": "2024/06/eef9f4aa33ca80e3f09e4c4661c6c80e.svg"
},
"519": {
"name": "Web",
"author": "000653.17c9b352828247bd858234a2a114f79b.1358",
"image": "2024/06/9c05e1e19cb5ecf6e156e44a8a8829e5.svg"
},
"536": {
"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"
} }
} }

View File

@@ -27,8 +27,8 @@ def draw_svg(track: dict, progress: int, model_id: str) -> str | None:
if progress is not None: if progress is not None:
paths = svg_content.split("L") paths = svg_content.split("L")
total = track.get("reduced_svg_content", {}).get(model_id, len(paths)) total = track.get("reduced_svg_content", {}).get(model_id, len(paths))
percent = (100 * progress) / total percent = min((100 * progress) / total, 100)
progress = math.floor((percent / 100) * len(paths)) progress = math.floor((percent / 100) * (len(paths) - 1))
svg = Element( svg = Element(
"svg", "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
@@ -22,6 +22,7 @@ from .pyoasismini.const import TRACKS
class OasisMiniSelectEntityDescription(SelectEntityDescription): class OasisMiniSelectEntityDescription(SelectEntityDescription):
"""Oasis Mini select entity description.""" """Oasis Mini select entity description."""
current_value: Callable[[OasisMini], Any]
select_fn: Callable[[OasisMini, int], Awaitable[None]] select_fn: Callable[[OasisMini, int], Awaitable[None]]
update_handler: Callable[[OasisMiniSelectEntity], None] | None = None update_handler: Callable[[OasisMiniSelectEntity], None] | None = None
@@ -30,6 +31,7 @@ class OasisMiniSelectEntity(OasisMiniEntity, SelectEntity):
"""Oasis Mini select entity.""" """Oasis Mini select entity."""
entity_description: OasisMiniSelectEntityDescription entity_description: OasisMiniSelectEntityDescription
_current_value: Any | None = None
def __init__( def __init__(
self, self,
@@ -46,8 +48,13 @@ 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)
if self._current_value == new_value:
return
self._current_value = new_value
if update_handler := self.entity_description.update_handler: if update_handler := self.entity_description.update_handler:
update_handler(self) update_handler(self)
else: else:
@@ -61,12 +68,21 @@ class OasisMiniSelectEntity(OasisMiniEntity, SelectEntity):
def playlist_update_handler(entity: OasisMiniSelectEntity) -> None: def playlist_update_handler(entity: OasisMiniSelectEntity) -> None:
"""Handle playlist updates.""" """Handle playlist updates."""
# pylint: disable=protected-access # pylint: disable=protected-access
device = entity.device
options = [ options = [
TRACKS.get(str(track), {}).get("name", str(track)) device._playlist.get(track, {}).get(
for track in entity.device.playlist "name",
TRACKS.get(str(track), {}).get(
"name",
device.track["name"]
if device.track and device.track["id"] == track
else str(track),
),
)
for track in device.playlist
] ]
entity._attr_options = options entity._attr_options = options
index = min(entity.device.playlist_index, len(options) - 1) index = min(device.playlist_index, len(options) - 1)
entity._attr_current_option = options[index] if options else None entity._attr_current_option = options[index] if options else None
@@ -74,6 +90,7 @@ DESCRIPTORS = (
OasisMiniSelectEntityDescription( OasisMiniSelectEntityDescription(
key="playlist", key="playlist",
name="Playlist", name="Playlist",
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,
), ),
@@ -81,6 +98,7 @@ DESCRIPTORS = (
key="autoplay", key="autoplay",
name="Autoplay", name="Autoplay",
options=list(AUTOPLAY_MAP.values()), options=list(AUTOPLAY_MAP.values()),
current_value=lambda device: device.autoplay,
select_fn=lambda device, option: device.async_set_autoplay(option), select_fn=lambda device, option: device.async_set_autoplay(option),
), ),
) )

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

@@ -26,6 +26,7 @@
"options": { "options": {
"step": { "step": {
"init": { "init": {
"description": "Add your cloud credentials to get additional information about your device",
"data": { "data": {
"email": "[%key:common::config_flow::data::email%]", "email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]" "password": "[%key:common::config_flow::data::password%]"
@@ -35,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": "Live drawing"
}
}
}
} }
} }

View File

@@ -26,6 +26,7 @@
"options": { "options": {
"step": { "step": {
"init": { "init": {
"description": "Add your cloud credentials to get additional information about your device",
"data": { "data": {
"email": "Email", "email": "Email",
"password": "Password" "password": "Password"
@@ -35,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": "Live drawing"
}
}
}
} }
} }