1
0
mirror of https://github.com/natekspencer/hacs-oasis_mini.git synced 2025-12-06 18:44:14 -05:00

14 Commits
2.1.0 ... main

Author SHA1 Message Date
Nathan Spencer
06a1e43295 Merge pull request #111 from natekspencer/update-tracks
Update tracks
2025-12-01 12:22:59 -07:00
natekspencer
f50320378b Update tracks 2025-12-01 19:15:40 +00:00
Nathan Spencer
505fca3635 Merge pull request #110 from natekspencer/last-updated-sensor
Add an additional last updated diagnostic sensor for devices
2025-11-26 15:04:19 -07:00
Nathan Spencer
cdca084212 Address PR comments 2025-11-26 22:01:02 +00:00
Nathan Spencer
dfaeb382da Add an additional last updated diagnostic sensor for devices 2025-11-26 21:53:56 +00:00
Nathan Spencer
8a2dc8e9bc Merge pull request #109 from natekspencer/set-playlist
Make device own required steps when setting playlist
2025-11-26 14:52:24 -07:00
Nathan Spencer
8467c50215 Address PR 2025-11-26 21:47:15 +00:00
Nathan Spencer
7d7675dcb1 Address PR comments 2025-11-26 21:36:23 +00:00
Nathan Spencer
fb360be616 Add additional keyword argument to set playlist to allow play control 2025-11-26 21:01:18 +00:00
Nathan Spencer
4336f658c4 Make device own required steps when setting playlist 2025-11-26 20:32:12 +00:00
Nathan Spencer
50773c582c Merge pull request #108 from natekspencer/validate-workflow
Updates the GitHub Actions validation workflow with configuration refinements
2025-11-26 13:08:50 -07:00
Nathan Spencer
461165673c Update validate workflow 2025-11-26 20:03:08 +00:00
Nathan Spencer
8d3cc00ebc Merge pull request #107 from natekspencer/track-image-url
Use helper to get image from track dictionary
2025-11-26 12:52:09 -07:00
Nathan Spencer
c4fd6a7ef6 Use helper to get image from track dictionary 2025-11-26 19:46:02 +00:00
9 changed files with 74 additions and 40 deletions

View File

@@ -1,22 +1,25 @@
name: Validate repo
on:
push:
pull_request:
push:
branches:
- main
schedule:
- cron: "0 0 * * *"
jobs:
hassfest:
name: Validate with hassfest
runs-on: "ubuntu-latest"
runs-on: ubuntu-latest
steps:
- uses: "actions/checkout@v4"
- uses: "home-assistant/actions/hassfest@master"
- uses: actions/checkout@v6
- uses: home-assistant/actions/hassfest@master
hacs:
name: Validate with HACS
runs-on: "ubuntu-latest"
runs-on: ubuntu-latest
steps:
- uses: "hacs/action@main"
- uses: hacs/action@main
with:
category: "integration"
category: integration

View File

@@ -17,7 +17,7 @@ from homeassistant.components.media_player import (
from .pyoasiscontrol import OasisCloudClient
from .pyoasiscontrol.const import TRACKS
from .pyoasiscontrol.utils import get_track_ids_from_playlist, get_url_for_image
from .pyoasiscontrol.utils import get_image_url_from_track, get_track_ids_from_playlist
_LOGGER = logging.getLogger(__name__)
@@ -128,7 +128,7 @@ def build_tracks_root() -> BrowseMedia:
media_content_type=MEDIA_TYPE_OASIS_TRACK,
can_play=True,
can_expand=False,
thumbnail=get_url_for_image(meta.get("image")),
thumbnail=get_image_url_from_track(meta),
)
for track_id, meta in TRACKS.items()
]
@@ -156,15 +156,15 @@ def build_track_item(track_id: int) -> BrowseMedia:
media_content_type=MEDIA_TYPE_OASIS_TRACK,
can_play=True,
can_expand=False,
thumbnail=get_url_for_image(meta.get("image")),
thumbnail=get_image_url_from_track(meta),
)
def get_first_image_for_playlist(playlist: dict[str, Any]) -> str | None:
"""Get the first image from a playlist dictionary."""
for track in playlist.get("patterns") or []:
if image := track.get("image"):
return get_url_for_image(image)
if image := get_image_url_from_track(track):
return image
return None

View File

@@ -340,9 +340,7 @@ class OasisDeviceMediaPlayerEntity(OasisDeviceEntity, MediaPlayerEntity):
return
if enqueue == MediaPlayerEnqueue.REPLACE:
await device.async_stop()
await device.async_set_playlist(track_ids)
await device.async_play()
await device.async_set_playlist(track_ids, start_playing=True)
return
insert_at = (device.playlist_index or 0) + 1

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
from datetime import datetime
import logging
from typing import TYPE_CHECKING, Any, Callable, Final, Iterable
@@ -11,6 +12,7 @@ from .const import (
LED_EFFECTS,
STATUS_CODE_MAP,
STATUS_ERROR,
STATUS_PLAYING,
STATUS_SLEEPING,
TRACKS,
)
@@ -19,7 +21,8 @@ from .utils import (
_parse_int,
create_svg,
decrypt_svg_content,
get_url_for_image,
get_image_url_from_track,
now,
)
if TYPE_CHECKING: # avoid runtime circular imports
@@ -139,6 +142,9 @@ class OasisDevice:
self._track: dict | None = None
self._track_task: asyncio.Task | None = None
# Diagnostic metadata
self.last_updated: datetime | None = None
@property
def brightness(self) -> int:
"""
@@ -258,6 +264,8 @@ class OasisDevice:
if changed:
self._notify_listeners()
self.last_updated = now()
def parse_status_string(self, raw_status: str) -> dict[str, Any] | None:
"""
Parse a semicolon-separated device status string into a structured state dictionary.
@@ -407,9 +415,7 @@ class OasisDevice:
Returns:
str: Full URL to the track image or `None` if no image is available.
"""
if track := self.track:
return get_url_for_image(track.get("image"))
return None
return get_image_url_from_track(self.track)
@property
def track_name(self) -> str | None:
@@ -647,21 +653,33 @@ class OasisDevice:
client = self._require_client()
await client.async_send_add_joblist_command(self, tracks)
async def async_set_playlist(self, playlist: int | Iterable[int]) -> None:
async def async_set_playlist(
self, playlist: int | Iterable[int], *, start_playing: bool | None = None
) -> None:
"""
Set the device's playlist to the provided track or tracks.
Accepts a single track ID or an iterable of track IDs and replaces the device's playlist by sending the corresponding command to the attached client.
Accepts a single track ID or an iterable of track IDs, stops the device,
replaces the playlist, and resumes playback based on the `start_playing`
parameter or, if unspecified, the device's prior playing state.
Parameters:
playlist (int | Iterable[int]): A single track ID or an iterable of track IDs to set as the new playlist.
playlist (int | Iterable[int]):
A single track ID or an iterable of track IDs to set as the new playlist.
start_playing (bool | None, keyword-only):
Whether to start playback after updating the playlist. If None (default),
playback will resume only if the device was previously playing and the
new playlist is non-empty.
"""
if isinstance(playlist, int):
playlist_list = [playlist]
else:
playlist_list = list(playlist)
playlist = [playlist] if isinstance(playlist, int) else list(playlist)
if start_playing is None:
start_playing = self.status_code == STATUS_PLAYING
client = self._require_client()
await client.async_send_set_playlist_command(self, playlist_list)
await client.async_send_stop_command(self) # needed before replacing playlist
await client.async_send_set_playlist_command(self, playlist)
if start_playing and playlist:
await client.async_send_play_command(self)
async def async_set_repeat_playlist(self, repeat: bool) -> None:
"""

View File

@@ -13622,7 +13622,7 @@
"pattern_id": null,
"clean_type": "clean_id",
"png_image": "2025/06/a008fed534a5d78dceda0072850cfc82.png",
"author": "Thalia soares",
"author": "Thalia vitoria",
"reduced_svg_content_new": 3823
},
"5965": {

View File

@@ -22,6 +22,8 @@ COLOR_LIGHT_SHADE = ("#FFFFFF", "#86888F")
COLOR_MEDIUM_SHADE = ("#E5E2DE", "#86888F")
COLOR_MEDIUM_TINT = ("#B8B8B8", "#FFFFFF")
IMAGE_URL = "https://app.grounded.so/uploads/{image}"
def _bit_to_bool(val: str) -> bool:
"""Convert a bit string to bool."""
@@ -200,15 +202,17 @@ def decrypt_svg_content(svg_content: dict[str, str]):
return decrypted
def get_image_url_from_track(track: dict[str, Any] | None) -> str | None:
"""Get the image URL from a track."""
if not isinstance(track, dict):
return None
return IMAGE_URL.format(image=image) if (image := track.get("image")) else None
def get_track_ids_from_playlist(playlist: dict[str, Any]) -> list[int]:
"""Get a list of track ids from a playlist."""
return [track["id"] for track in (playlist.get("patterns") or []) if "id" in track]
def get_url_for_image(image: str | None) -> str | None:
"""Get the full URL for an image."""
return f"https://app.grounded.so/uploads/{image}" if image else None
def now() -> datetime:
return datetime.now(UTC)

View File

@@ -2,7 +2,10 @@
from __future__ import annotations
from datetime import datetime
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
@@ -68,6 +71,13 @@ DESCRIPTORS = [
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
SensorEntityDescription(
key="last_updated",
translation_key="last_updated",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
]
DESCRIPTORS.extend(
SensorEntityDescription(
@@ -84,11 +94,6 @@ class OasisDeviceSensorEntity(OasisDeviceEntity, SensorEntity):
"""Oasis device sensor entity."""
@property
def native_value(self) -> str | None:
"""
Provide the current sensor value from the underlying device.
Returns:
`str` with the sensor's current value, or `None` if the attribute is not present or has no value. The value is taken from the device attribute named by the entity description's `key`.
"""
def native_value(self) -> str | int | float | datetime | None:
"""Provide the current sensor value from the underlying device."""
return getattr(self.device, self.entity_description.key)

View File

@@ -125,6 +125,9 @@
"18": "Error while downloading the job file"
}
},
"last_updated": {
"name": "Last updated"
},
"led_color_id": {
"name": "LED color ID"
},

View File

@@ -125,6 +125,9 @@
"18": "Error while downloading the job file"
}
},
"last_updated": {
"name": "Last updated"
},
"led_color_id": {
"name": "LED color ID"
},