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

Get track info from the cloud when playlist or index changes

This commit is contained in:
Nathan Spencer
2025-11-23 00:13:45 +00:00
parent ecad472bbd
commit 2a92212aad
6 changed files with 82 additions and 20 deletions

View File

@@ -10,9 +10,9 @@
<img alt="Oasis Mini logo" src="https://brands.home-assistant.io/oasis_mini/logo.png">
</picture>
# Oasis Mini for Home Assistant
# Oasis Control for Home Assistant
Home Assistant integration for Oasis Mini kinetic sand art devices.
Home Assistant integration for Oasis kinetic sand art devices.
# Installation
@@ -43,13 +43,13 @@ While the manual installation above seems like less steps, it's important to not
[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=oasis_mini)
There is a config flow for this Oasis Mini integration. After installing the custom component, use the convenient My Home Assistant link above.
There is a config flow for this Oasis Control integration. After installing the custom component, use the convenient My Home Assistant link above.
Alternatively:
1. Go to **Configuration**->**Integrations**
2. Click **+ ADD INTEGRATION** to setup a new integration
3. Search for **Oasis Mini** and click on it
3. Search for **Oasis Control** and click on it
4. You will be guided through the rest of the setup process via the config flow
# Options
@@ -76,6 +76,6 @@ data:
I'm not employed by Kinetic Oasis, and provide this custom component purely for your own enjoyment and home automation needs.
If you already own an Oasis Mini, found this integration useful and want to donate, consider [sponsoring me on GitHub](https://github.com/sponsors/natekspencer) or buying me a coffee ☕ (or beer 🍺) instead by using the link below:
If you already own an Oasis device, found this integration useful and want to donate, consider [sponsoring me on GitHub](https://github.com/sponsors/natekspencer) or buying me a coffee ☕ (or beer 🍺) instead by using the link below:
<a href='https://ko-fi.com/Y8Y57F59S' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi1.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>

View File

@@ -10,6 +10,7 @@ from homeassistant.const import CONF_EMAIL, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed
import homeassistant.helpers.entity_registry as er
import homeassistant.util.dt as dt_util
from .coordinator import OasisDeviceCoordinator
from .helpers import create_client
@@ -59,6 +60,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: OasisDeviceConfigEntry)
entry.runtime_data = coordinator
def _on_oasis_update() -> None:
coordinator.last_updated = dt_util.now()
coordinator.async_update_listeners()
for device in coordinator.data:

View File

@@ -9,6 +9,7 @@ import async_timeout
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
import homeassistant.util.dt as dt_util
from .const import DOMAIN
from .pyoasiscontrol import OasisCloudClient, OasisDevice, OasisMqttClient
@@ -52,6 +53,7 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]):
OasisDevice(
model=raw_device.get("model", {}).get("name"),
serial_number=raw_device.get("serial_number"),
cloud=self.cloud_client,
)
for raw_device in raw_devices
]
@@ -60,8 +62,10 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]):
for device in devices:
self.mqtt_client.register_device(device)
await self.mqtt_client.wait_until_ready(device, request_status=True)
if not device.mac_address:
await device.async_get_mac_address()
if not await device.async_get_mac_address():
raise Exception(
"Could not get mac address for %s", device.serial_number
)
# if not device.software_version:
# await device.async_get_software_version()
# data = await self.device.async_get_status()
@@ -77,5 +81,5 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]):
) from ex
if devices != self.data:
self.last_updated = datetime.now()
self.last_updated = dt_util.now()
return devices

View File

@@ -30,8 +30,12 @@ class OasisDeviceEntity(CoordinatorEntity[OasisDeviceCoordinator]):
serial_number = device.serial_number
self._attr_unique_id = f"{serial_number}-{description.key}"
connections = set()
if mac_address := device.mac_address:
connections.add((CONNECTION_NETWORK_MAC, format_mac(mac_address)))
self._attr_device_info = DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, format_mac(device.mac_address))},
connections=connections,
identifiers={(DOMAIN, serial_number)},
name=f"{device.model} {serial_number}",
manufacturer=device.manufacturer,

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
import logging
from typing import TYPE_CHECKING, Any, Callable, Final, Iterable
@@ -12,9 +13,10 @@ from .const import (
STATUS_CODE_SLEEPING,
TRACKS,
)
from .utils import _bit_to_bool, _parse_int
from .utils import _bit_to_bool, _parse_int, decrypt_svg_content
if TYPE_CHECKING: # avoid runtime circular imports
from .clients import OasisCloudClient
from .clients.transport import OasisClientProtocol
_LOGGER = logging.getLogger(__name__)
@@ -62,10 +64,12 @@ class OasisDevice:
serial_number: str | None = None,
ssid: str | None = None,
ip_address: str | None = None,
cloud: OasisCloudClient | None = None,
client: OasisClientProtocol | None = None,
) -> None:
# Transport
self._client: OasisClientProtocol | None = client
self._cloud = cloud
self._client = client
self._listeners: list[Callable[[], None]] = []
# Details
@@ -105,8 +109,9 @@ class OasisDevice:
self.environment: str | None = None
self.schedule: Any | None = None
# Track metadata cache (used if you hydrate from cloud)
# Track metadata cache
self._track: dict | None = None
self._track_task: asyncio.Task | None = None
@property
def brightness(self) -> int:
@@ -157,13 +162,20 @@ class OasisDevice:
def update_from_status_dict(self, data: dict[str, Any]) -> None:
"""Update device fields from a status payload (from any transport)."""
changed = False
playlist_or_index_changed = False
for key, value in data.items():
if hasattr(self, key):
if self._update_field(key, value):
changed = True
if key in ("playlist", "playlist_index"):
playlist_or_index_changed = True
else:
_LOGGER.warning("Unknown field: %s=%s", key, value)
if playlist_or_index_changed:
self._schedule_track_refresh()
if changed:
self._notify_listeners()
@@ -263,13 +275,13 @@ class OasisDevice:
@property
def drawing_progress(self) -> float | None:
"""Return drawing progress percentage for the current track."""
# if not (self.track and (svg_content := self.track.get("svg_content"))):
# return None
# svg_content = decrypt_svg_content(svg_content)
# paths = svg_content.split("L")
total = self.track.get("reduced_svg_content_new", 0) # or len(paths)
if not (self.track and (svg_content := self.track.get("svg_content"))):
return None
svg_content = decrypt_svg_content(svg_content)
paths = svg_content.split("L")
total = self.track.get("reduced_svg_content_new", 0) or len(paths)
percent = (100 * self.progress) / total
return percent
return max(percent, 100)
@property
def playlist_details(self) -> dict[int, dict[str, str]]:
@@ -330,7 +342,7 @@ class OasisDevice:
led_speed: int | None = None,
brightness: int | None = None,
) -> None:
"""Set the Oasis Mini LED (shared validation & attribute updates)."""
"""Set the Oasis device LED (shared validation & attribute updates)."""
if led_effect is None:
led_effect = self.led_effect
if color is None:
@@ -410,3 +422,43 @@ class OasisDevice:
async def async_reboot(self) -> None:
client = self._require_client()
await client.async_send_reboot_command(self)
def _schedule_track_refresh(self) -> None:
"""Schedule an async refresh of current track info if track_id changed."""
if not self._cloud:
return
try:
loop = asyncio.get_running_loop()
except RuntimeError:
_LOGGER.debug("No running loop; cannot schedule track refresh")
return
if self._track_task and not self._track_task.done():
self._track_task.cancel()
self._track_task = loop.create_task(self._async_refresh_current_track())
async def _async_refresh_current_track(self) -> None:
"""Refresh the current track info."""
if not self._cloud:
return
if (track_id := self.track_id) is None:
self._track = None
return
if self._track and self._track.get("id") == track_id:
return
try:
track = await self._cloud.async_get_track_info(track_id)
except Exception: # noqa: BLE001
_LOGGER.exception("Error fetching track info for %s", track_id)
return
if not track:
return
self._track = track
self._notify_listeners()

View File

@@ -1,4 +1,4 @@
"""Oasis Mini utils."""
"""Oasis control utils."""
from __future__ import annotations