mirror of
https://github.com/natekspencer/hacs-oasis_mini.git
synced 2025-12-06 18:44:14 -05:00
Dynamically handle devices and other enhancements
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
from urllib.parse import urljoin
|
||||
@@ -16,6 +17,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
BASE_URL = "https://app.grounded.so"
|
||||
PLAYLISTS_REFRESH_LIMITER = timedelta(minutes=5)
|
||||
SOFTWARE_REFRESH_LIMITER = timedelta(hours=1)
|
||||
|
||||
|
||||
class OasisCloudClient:
|
||||
@@ -32,15 +34,6 @@ class OasisCloudClient:
|
||||
* latest software metadata
|
||||
"""
|
||||
|
||||
_session: ClientSession | None
|
||||
_owns_session: bool
|
||||
_access_token: str | None
|
||||
|
||||
# these are "cache" fields for tracks/playlists
|
||||
_playlists_next_refresh: datetime
|
||||
playlists: list[dict[str, Any]]
|
||||
_playlist_details: dict[int, dict[str, str]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -51,10 +44,17 @@ class OasisCloudClient:
|
||||
self._owns_session = session is None
|
||||
self._access_token = access_token
|
||||
|
||||
# simple in-memory caches
|
||||
# playlists cache
|
||||
self.playlists: list[dict[str, Any]] = []
|
||||
self._playlists_next_refresh = now()
|
||||
self.playlists = []
|
||||
self._playlist_details = {}
|
||||
self._playlists_lock = asyncio.Lock()
|
||||
|
||||
self._playlist_details: dict[int, dict[str, str]] = {}
|
||||
|
||||
# software metadata cache
|
||||
self._software_details: dict[str, int | str] | None = None
|
||||
self._software_next_refresh = now()
|
||||
self._software_lock = asyncio.Lock()
|
||||
|
||||
@property
|
||||
def session(self) -> ClientSession:
|
||||
@@ -105,15 +105,32 @@ class OasisCloudClient:
|
||||
self, personal_only: bool = False
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get playlists from the cloud (cached by PLAYLISTS_REFRESH_LIMITER)."""
|
||||
if self._playlists_next_refresh <= now():
|
||||
now_dt = now()
|
||||
|
||||
def _is_cache_valid() -> bool:
|
||||
return self._playlists_next_refresh > now_dt and bool(self.playlists)
|
||||
|
||||
if _is_cache_valid():
|
||||
return self.playlists
|
||||
|
||||
async with self._playlists_lock:
|
||||
# Double-check in case another task just refreshed it
|
||||
now_dt = now()
|
||||
if _is_cache_valid():
|
||||
return self.playlists
|
||||
|
||||
params = {"my_playlists": str(personal_only).lower()}
|
||||
playlists = await self._async_auth_request(
|
||||
"GET", "api/playlist", params=params
|
||||
)
|
||||
if playlists:
|
||||
self.playlists = playlists
|
||||
self._playlists_next_refresh = now() + PLAYLISTS_REFRESH_LIMITER
|
||||
return self.playlists
|
||||
|
||||
if not isinstance(playlists, list):
|
||||
playlists = []
|
||||
|
||||
self.playlists = playlists
|
||||
self._playlists_next_refresh = now_dt + PLAYLISTS_REFRESH_LIMITER
|
||||
|
||||
return self.playlists
|
||||
|
||||
async def async_get_track_info(self, track_id: int) -> dict[str, Any] | None:
|
||||
"""Get single track info from the cloud."""
|
||||
@@ -143,9 +160,37 @@ class OasisCloudClient:
|
||||
track_details += response.get("data", [])
|
||||
return track_details
|
||||
|
||||
async def async_get_latest_software_details(self) -> dict[str, int | str]:
|
||||
"""Get latest software metadata from cloud."""
|
||||
return await self._async_auth_request("GET", "api/software/last-version")
|
||||
async def async_get_latest_software_details(
|
||||
self, *, force_refresh: bool = False
|
||||
) -> dict[str, int | str] | None:
|
||||
"""Get latest software metadata from cloud (cached)."""
|
||||
now_dt = now()
|
||||
|
||||
def _is_cache_valid() -> bool:
|
||||
return (
|
||||
not force_refresh
|
||||
and self._software_details is not None
|
||||
and self._software_next_refresh > now_dt
|
||||
)
|
||||
|
||||
if _is_cache_valid():
|
||||
return self._software_details
|
||||
|
||||
async with self._software_lock:
|
||||
# Double-check in case another task just refreshed it
|
||||
now_dt = now()
|
||||
if _is_cache_valid():
|
||||
return self._software_details
|
||||
|
||||
details = await self._async_auth_request("GET", "api/software/last-version")
|
||||
|
||||
if not isinstance(details, dict):
|
||||
details = {}
|
||||
|
||||
self._software_details = details
|
||||
self._software_next_refresh = now_dt + SOFTWARE_REFRESH_LIMITER
|
||||
|
||||
return self._software_details
|
||||
|
||||
async def _async_auth_request(self, method: str, url: str, **kwargs: Any) -> Any:
|
||||
"""Perform an authenticated cloud request."""
|
||||
|
||||
@@ -7,7 +7,7 @@ import base64
|
||||
from datetime import UTC, datetime
|
||||
import logging
|
||||
import ssl
|
||||
from typing import Any, Final
|
||||
from typing import Any, Final, Iterable
|
||||
|
||||
import aiomqtt
|
||||
|
||||
@@ -92,6 +92,11 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
"Could not schedule subscription for %s (no running loop)", serial
|
||||
)
|
||||
|
||||
def register_devices(self, devices: Iterable[OasisDevice]) -> None:
|
||||
"""Convenience method to register multiple devices."""
|
||||
for device in devices:
|
||||
self.register_device(device)
|
||||
|
||||
def unregister_device(self, device: OasisDevice) -> None:
|
||||
serial = device.serial_number
|
||||
if not serial:
|
||||
@@ -260,6 +265,13 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
|
||||
return device.mac_address
|
||||
|
||||
async def async_send_auto_clean_command(
|
||||
self, device: OasisDevice, auto_clean: bool
|
||||
) -> None:
|
||||
"""Send auto clean command."""
|
||||
payload = f"WRIAUTOCLEAN={1 if auto_clean else 0}"
|
||||
await self._publish_command(device, payload)
|
||||
|
||||
async def async_send_ball_speed_command(
|
||||
self,
|
||||
device: OasisDevice,
|
||||
|
||||
@@ -16,6 +16,10 @@ class OasisClientProtocol(Protocol):
|
||||
|
||||
async def async_get_mac_address(self, device: OasisDevice) -> str | None: ...
|
||||
|
||||
async def async_send_auto_clean_command(
|
||||
self, device: OasisDevice, auto_clean: bool
|
||||
) -> None: ...
|
||||
|
||||
async def async_send_ball_speed_command(
|
||||
self,
|
||||
device: OasisDevice,
|
||||
|
||||
@@ -23,6 +23,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
BALL_SPEED_MAX: Final = 400
|
||||
BALL_SPEED_MIN: Final = 100
|
||||
BRIGHTNESS_DEFAULT: Final = 100
|
||||
LED_SPEED_MAX: Final = 90
|
||||
LED_SPEED_MIN: Final = -90
|
||||
|
||||
@@ -62,6 +63,7 @@ class OasisDevice:
|
||||
*,
|
||||
model: str | None = None,
|
||||
serial_number: str | None = None,
|
||||
name: str | None = None,
|
||||
ssid: str | None = None,
|
||||
ip_address: str | None = None,
|
||||
cloud: OasisCloudClient | None = None,
|
||||
@@ -73,10 +75,11 @@ class OasisDevice:
|
||||
self._listeners: list[Callable[[], None]] = []
|
||||
|
||||
# Details
|
||||
self.model: str | None = model
|
||||
self.serial_number: str | None = serial_number
|
||||
self.ssid: str | None = ssid
|
||||
self.ip_address: str | None = ip_address
|
||||
self.model = model
|
||||
self.serial_number = serial_number
|
||||
self.name = name if name else f"{model} {serial_number}"
|
||||
self.ssid = ssid
|
||||
self.ip_address = ip_address
|
||||
|
||||
# Status
|
||||
self.auto_clean: bool = False
|
||||
@@ -84,7 +87,7 @@ class OasisDevice:
|
||||
self.ball_speed: int = BALL_SPEED_MIN
|
||||
self._brightness: int = 0
|
||||
self.brightness_max: int = 200
|
||||
self.brightness_on: int = 0
|
||||
self.brightness_on: int = BRIGHTNESS_DEFAULT
|
||||
self.busy: bool = False
|
||||
self.color: str | None = None
|
||||
self.download_progress: int = 0
|
||||
@@ -150,7 +153,8 @@ class OasisDevice:
|
||||
old = getattr(self, name, None)
|
||||
if old != value:
|
||||
_LOGGER.debug(
|
||||
"%s changed: '%s' -> '%s'",
|
||||
"%s %s changed: '%s' -> '%s'",
|
||||
self.serial_number,
|
||||
name.replace("_", " ").capitalize(),
|
||||
old,
|
||||
value,
|
||||
@@ -174,7 +178,7 @@ class OasisDevice:
|
||||
_LOGGER.warning("Unknown field: %s=%s", key, value)
|
||||
|
||||
if playlist_or_index_changed:
|
||||
self._schedule_track_refresh()
|
||||
self.schedule_track_refresh()
|
||||
|
||||
if changed:
|
||||
self._notify_listeners()
|
||||
@@ -343,6 +347,10 @@ class OasisDevice:
|
||||
self._update_field("mac_address", mac)
|
||||
return mac
|
||||
|
||||
async def async_set_auto_clean(self, auto_clean: bool) -> None:
|
||||
client = self._require_client()
|
||||
await client.async_send_auto_clean_command(self, auto_clean)
|
||||
|
||||
async def async_set_ball_speed(self, speed: int) -> None:
|
||||
if not BALL_SPEED_MIN <= speed <= BALL_SPEED_MAX:
|
||||
raise ValueError("Invalid speed specified")
|
||||
@@ -438,7 +446,7 @@ class OasisDevice:
|
||||
client = self._require_client()
|
||||
await client.async_send_reboot_command(self)
|
||||
|
||||
def _schedule_track_refresh(self) -> None:
|
||||
def schedule_track_refresh(self) -> None:
|
||||
"""Schedule an async refresh of current track info if track_id changed."""
|
||||
if not self._cloud:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user