mirror of
https://github.com/natekspencer/hacs-oasis_mini.git
synced 2025-12-06 18:44:14 -05:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06a1e43295 | ||
|
|
f50320378b | ||
|
|
505fca3635 | ||
|
|
cdca084212 | ||
|
|
dfaeb382da | ||
|
|
8a2dc8e9bc | ||
|
|
8467c50215 | ||
|
|
7d7675dcb1 | ||
|
|
fb360be616 | ||
|
|
4336f658c4 | ||
|
|
50773c582c | ||
|
|
461165673c | ||
|
|
8d3cc00ebc | ||
|
|
c4fd6a7ef6 |
17
.github/workflows/validate.yaml
vendored
17
.github/workflows/validate.yaml
vendored
@@ -1,22 +1,25 @@
|
|||||||
name: Validate repo
|
name: Validate repo
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 0 * * *"
|
- cron: "0 0 * * *"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
hassfest:
|
hassfest:
|
||||||
name: Validate with hassfest
|
name: Validate with hassfest
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: "actions/checkout@v4"
|
- uses: actions/checkout@v6
|
||||||
- uses: "home-assistant/actions/hassfest@master"
|
- uses: home-assistant/actions/hassfest@master
|
||||||
|
|
||||||
hacs:
|
hacs:
|
||||||
name: Validate with HACS
|
name: Validate with HACS
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: "hacs/action@main"
|
- uses: hacs/action@main
|
||||||
with:
|
with:
|
||||||
category: "integration"
|
category: integration
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from homeassistant.components.media_player import (
|
|||||||
|
|
||||||
from .pyoasiscontrol import OasisCloudClient
|
from .pyoasiscontrol import OasisCloudClient
|
||||||
from .pyoasiscontrol.const import TRACKS
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ def build_tracks_root() -> BrowseMedia:
|
|||||||
media_content_type=MEDIA_TYPE_OASIS_TRACK,
|
media_content_type=MEDIA_TYPE_OASIS_TRACK,
|
||||||
can_play=True,
|
can_play=True,
|
||||||
can_expand=False,
|
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()
|
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,
|
media_content_type=MEDIA_TYPE_OASIS_TRACK,
|
||||||
can_play=True,
|
can_play=True,
|
||||||
can_expand=False,
|
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:
|
def get_first_image_for_playlist(playlist: dict[str, Any]) -> str | None:
|
||||||
"""Get the first image from a playlist dictionary."""
|
"""Get the first image from a playlist dictionary."""
|
||||||
for track in playlist.get("patterns") or []:
|
for track in playlist.get("patterns") or []:
|
||||||
if image := track.get("image"):
|
if image := get_image_url_from_track(track):
|
||||||
return get_url_for_image(image)
|
return image
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -340,9 +340,7 @@ class OasisDeviceMediaPlayerEntity(OasisDeviceEntity, MediaPlayerEntity):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if enqueue == MediaPlayerEnqueue.REPLACE:
|
if enqueue == MediaPlayerEnqueue.REPLACE:
|
||||||
await device.async_stop()
|
await device.async_set_playlist(track_ids, start_playing=True)
|
||||||
await device.async_set_playlist(track_ids)
|
|
||||||
await device.async_play()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
insert_at = (device.playlist_index or 0) + 1
|
insert_at = (device.playlist_index or 0) + 1
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Final, Iterable
|
from typing import TYPE_CHECKING, Any, Callable, Final, Iterable
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ from .const import (
|
|||||||
LED_EFFECTS,
|
LED_EFFECTS,
|
||||||
STATUS_CODE_MAP,
|
STATUS_CODE_MAP,
|
||||||
STATUS_ERROR,
|
STATUS_ERROR,
|
||||||
|
STATUS_PLAYING,
|
||||||
STATUS_SLEEPING,
|
STATUS_SLEEPING,
|
||||||
TRACKS,
|
TRACKS,
|
||||||
)
|
)
|
||||||
@@ -19,7 +21,8 @@ from .utils import (
|
|||||||
_parse_int,
|
_parse_int,
|
||||||
create_svg,
|
create_svg,
|
||||||
decrypt_svg_content,
|
decrypt_svg_content,
|
||||||
get_url_for_image,
|
get_image_url_from_track,
|
||||||
|
now,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING: # avoid runtime circular imports
|
if TYPE_CHECKING: # avoid runtime circular imports
|
||||||
@@ -139,6 +142,9 @@ class OasisDevice:
|
|||||||
self._track: dict | None = None
|
self._track: dict | None = None
|
||||||
self._track_task: asyncio.Task | None = None
|
self._track_task: asyncio.Task | None = None
|
||||||
|
|
||||||
|
# Diagnostic metadata
|
||||||
|
self.last_updated: datetime | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brightness(self) -> int:
|
def brightness(self) -> int:
|
||||||
"""
|
"""
|
||||||
@@ -258,6 +264,8 @@ class OasisDevice:
|
|||||||
if changed:
|
if changed:
|
||||||
self._notify_listeners()
|
self._notify_listeners()
|
||||||
|
|
||||||
|
self.last_updated = now()
|
||||||
|
|
||||||
def parse_status_string(self, raw_status: str) -> dict[str, Any] | None:
|
def parse_status_string(self, raw_status: str) -> dict[str, Any] | None:
|
||||||
"""
|
"""
|
||||||
Parse a semicolon-separated device status string into a structured state dictionary.
|
Parse a semicolon-separated device status string into a structured state dictionary.
|
||||||
@@ -407,9 +415,7 @@ class OasisDevice:
|
|||||||
Returns:
|
Returns:
|
||||||
str: Full URL to the track image or `None` if no image is available.
|
str: Full URL to the track image or `None` if no image is available.
|
||||||
"""
|
"""
|
||||||
if track := self.track:
|
return get_image_url_from_track(self.track)
|
||||||
return get_url_for_image(track.get("image"))
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def track_name(self) -> str | None:
|
def track_name(self) -> str | None:
|
||||||
@@ -647,21 +653,33 @@ class OasisDevice:
|
|||||||
client = self._require_client()
|
client = self._require_client()
|
||||||
await client.async_send_add_joblist_command(self, tracks)
|
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.
|
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:
|
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 = [playlist] if isinstance(playlist, int) else list(playlist)
|
||||||
playlist_list = [playlist]
|
if start_playing is None:
|
||||||
else:
|
start_playing = self.status_code == STATUS_PLAYING
|
||||||
playlist_list = list(playlist)
|
|
||||||
client = self._require_client()
|
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:
|
async def async_set_repeat_playlist(self, repeat: bool) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -13622,7 +13622,7 @@
|
|||||||
"pattern_id": null,
|
"pattern_id": null,
|
||||||
"clean_type": "clean_id",
|
"clean_type": "clean_id",
|
||||||
"png_image": "2025/06/a008fed534a5d78dceda0072850cfc82.png",
|
"png_image": "2025/06/a008fed534a5d78dceda0072850cfc82.png",
|
||||||
"author": "Thalia soares",
|
"author": "Thalia vitoria",
|
||||||
"reduced_svg_content_new": 3823
|
"reduced_svg_content_new": 3823
|
||||||
},
|
},
|
||||||
"5965": {
|
"5965": {
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ COLOR_LIGHT_SHADE = ("#FFFFFF", "#86888F")
|
|||||||
COLOR_MEDIUM_SHADE = ("#E5E2DE", "#86888F")
|
COLOR_MEDIUM_SHADE = ("#E5E2DE", "#86888F")
|
||||||
COLOR_MEDIUM_TINT = ("#B8B8B8", "#FFFFFF")
|
COLOR_MEDIUM_TINT = ("#B8B8B8", "#FFFFFF")
|
||||||
|
|
||||||
|
IMAGE_URL = "https://app.grounded.so/uploads/{image}"
|
||||||
|
|
||||||
|
|
||||||
def _bit_to_bool(val: str) -> bool:
|
def _bit_to_bool(val: str) -> bool:
|
||||||
"""Convert a bit string to bool."""
|
"""Convert a bit string to bool."""
|
||||||
@@ -200,15 +202,17 @@ def decrypt_svg_content(svg_content: dict[str, str]):
|
|||||||
return decrypted
|
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]:
|
def get_track_ids_from_playlist(playlist: dict[str, Any]) -> list[int]:
|
||||||
"""Get a list of track ids from a playlist."""
|
"""Get a list of track ids from a playlist."""
|
||||||
return [track["id"] for track in (playlist.get("patterns") or []) if "id" in track]
|
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:
|
def now() -> datetime:
|
||||||
return datetime.now(UTC)
|
return datetime.now(UTC)
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
@@ -68,6 +71,13 @@ DESCRIPTORS = [
|
|||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
suggested_display_precision=1,
|
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(
|
DESCRIPTORS.extend(
|
||||||
SensorEntityDescription(
|
SensorEntityDescription(
|
||||||
@@ -84,11 +94,6 @@ class OasisDeviceSensorEntity(OasisDeviceEntity, SensorEntity):
|
|||||||
"""Oasis device sensor entity."""
|
"""Oasis device sensor entity."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> str | None:
|
def native_value(self) -> str | int | float | datetime | None:
|
||||||
"""
|
"""Provide the current sensor value from the underlying device."""
|
||||||
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`.
|
|
||||||
"""
|
|
||||||
return getattr(self.device, self.entity_description.key)
|
return getattr(self.device, self.entity_description.key)
|
||||||
|
|||||||
@@ -125,6 +125,9 @@
|
|||||||
"18": "Error while downloading the job file"
|
"18": "Error while downloading the job file"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"last_updated": {
|
||||||
|
"name": "Last updated"
|
||||||
|
},
|
||||||
"led_color_id": {
|
"led_color_id": {
|
||||||
"name": "LED color ID"
|
"name": "LED color ID"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -125,6 +125,9 @@
|
|||||||
"18": "Error while downloading the job file"
|
"18": "Error while downloading the job file"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"last_updated": {
|
||||||
|
"name": "Last updated"
|
||||||
|
},
|
||||||
"led_color_id": {
|
"led_color_id": {
|
||||||
"name": "LED color ID"
|
"name": "LED color ID"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user