mirror of
https://github.com/natekspencer/hacs-oasis_mini.git
synced 2025-12-06 18:44:14 -05:00
📝 Add docstrings to mqtt
Docstrings generation was requested by @natekspencer. * https://github.com/natekspencer/hacs-oasis_mini/pull/98#issuecomment-3568450288 The following files were modified: * `custom_components/oasis_mini/__init__.py` * `custom_components/oasis_mini/binary_sensor.py` * `custom_components/oasis_mini/button.py` * `custom_components/oasis_mini/config_flow.py` * `custom_components/oasis_mini/coordinator.py` * `custom_components/oasis_mini/entity.py` * `custom_components/oasis_mini/helpers.py` * `custom_components/oasis_mini/image.py` * `custom_components/oasis_mini/light.py` * `custom_components/oasis_mini/media_player.py` * `custom_components/oasis_mini/number.py` * `custom_components/oasis_mini/pyoasiscontrol/clients/cloud_client.py` * `custom_components/oasis_mini/pyoasiscontrol/clients/http_client.py` * `custom_components/oasis_mini/pyoasiscontrol/clients/mqtt_client.py` * `custom_components/oasis_mini/pyoasiscontrol/clients/transport.py` * `custom_components/oasis_mini/pyoasiscontrol/device.py` * `custom_components/oasis_mini/pyoasiscontrol/utils.py` * `custom_components/oasis_mini/select.py` * `custom_components/oasis_mini/sensor.py` * `custom_components/oasis_mini/switch.py` * `custom_components/oasis_mini/update.py` * `update_tracks.py`
This commit is contained in:
committed by
GitHub
parent
cf21a5d995
commit
4ef28fc741
@@ -40,6 +40,15 @@ class OasisCloudClient:
|
||||
session: ClientSession | None = None,
|
||||
access_token: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the OasisCloudClient.
|
||||
|
||||
Sets the optional aiohttp session and access token, records whether the client owns the session, and initializes caches and asyncio locks for playlists and software metadata.
|
||||
|
||||
Parameters:
|
||||
session (ClientSession | None): Optional aiohttp ClientSession to use. If None, the client will create and own a session later.
|
||||
access_token (str | None): Optional initial access token for authenticated requests.
|
||||
"""
|
||||
self._session = session
|
||||
self._owns_session = session is None
|
||||
self._access_token = access_token
|
||||
@@ -58,27 +67,52 @@ class OasisCloudClient:
|
||||
|
||||
@property
|
||||
def session(self) -> ClientSession:
|
||||
"""Return (or lazily create) the aiohttp ClientSession."""
|
||||
"""
|
||||
Get the active aiohttp ClientSession, creating and owning a new session if none exists or the existing session is closed.
|
||||
|
||||
Returns:
|
||||
ClientSession: The active aiohttp ClientSession; a new session is created and marked as owned by this client when necessary.
|
||||
"""
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = ClientSession()
|
||||
self._owns_session = True
|
||||
return self._session
|
||||
|
||||
async def async_close(self) -> None:
|
||||
"""Close owned session (call from HA unload / cleanup)."""
|
||||
"""
|
||||
Close the aiohttp ClientSession owned by this client if it exists and is open.
|
||||
|
||||
This should be called during teardown when the client is responsible for the session.
|
||||
"""
|
||||
if self._session and not self._session.closed and self._owns_session:
|
||||
await self._session.close()
|
||||
|
||||
@property
|
||||
def access_token(self) -> str | None:
|
||||
"""
|
||||
Access token used for authenticated requests or None if not set.
|
||||
|
||||
Returns:
|
||||
The current access token string, or `None` if no token is stored.
|
||||
"""
|
||||
return self._access_token
|
||||
|
||||
@access_token.setter
|
||||
def access_token(self, value: str | None) -> None:
|
||||
"""
|
||||
Set the access token used for authenticated requests.
|
||||
|
||||
Parameters:
|
||||
value (str | None): The bearer token to store; pass None to clear the stored token.
|
||||
"""
|
||||
self._access_token = value
|
||||
|
||||
async def async_login(self, email: str, password: str) -> None:
|
||||
"""Login via the cloud and store the access token."""
|
||||
"""
|
||||
Log in to the Oasis cloud and store the received access token on the client.
|
||||
|
||||
Performs an authentication request using the provided credentials and saves the returned access token to self.access_token for use in subsequent authenticated requests.
|
||||
"""
|
||||
response = await self._async_request(
|
||||
"POST",
|
||||
urljoin(BASE_URL, "api/auth/login"),
|
||||
@@ -89,25 +123,58 @@ class OasisCloudClient:
|
||||
_LOGGER.debug("Cloud login succeeded, token set: %s", bool(token))
|
||||
|
||||
async def async_logout(self) -> None:
|
||||
"""Logout from the cloud."""
|
||||
"""
|
||||
End the current authenticated session with the Oasis cloud.
|
||||
|
||||
Performs a logout request and clears the stored access token on success.
|
||||
"""
|
||||
await self._async_auth_request("GET", "api/auth/logout")
|
||||
self.access_token = None
|
||||
|
||||
async def async_get_user(self) -> dict:
|
||||
"""Get current user info."""
|
||||
"""
|
||||
Return information about the currently authenticated user.
|
||||
|
||||
Returns:
|
||||
dict: A mapping containing the user's details as returned by the cloud API.
|
||||
|
||||
Raises:
|
||||
UnauthenticatedError: If no access token is available or the request is unauthorized.
|
||||
"""
|
||||
return await self._async_auth_request("GET", "api/auth/user")
|
||||
|
||||
async def async_get_devices(self) -> list[dict[str, Any]]:
|
||||
"""Get user devices (raw JSON from API)."""
|
||||
"""
|
||||
Retrieve the current user's devices from the cloud API.
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]: A list of device objects as returned by the API.
|
||||
"""
|
||||
return await self._async_auth_request("GET", "api/user/devices")
|
||||
|
||||
async def async_get_playlists(
|
||||
self, personal_only: bool = False
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get playlists from the cloud (cached by PLAYLISTS_REFRESH_LIMITER)."""
|
||||
"""
|
||||
Retrieve playlists from the Oasis cloud, optionally limited to the authenticated user's personal playlists.
|
||||
|
||||
The result is cached and will be refreshed according to PLAYLISTS_REFRESH_LIMITER to avoid frequent network requests.
|
||||
|
||||
Parameters:
|
||||
personal_only (bool): If True, return only playlists owned by the authenticated user; otherwise return all available playlists.
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]: A list of playlist objects represented as dictionaries; an empty list if no playlists are available.
|
||||
"""
|
||||
now_dt = now()
|
||||
|
||||
def _is_cache_valid() -> bool:
|
||||
"""
|
||||
Determine whether the playlists cache is still valid.
|
||||
|
||||
Returns:
|
||||
`true` if the playlists cache contains data and the next refresh timestamp is later than the current time, `false` otherwise.
|
||||
"""
|
||||
return self._playlists_next_refresh > now_dt and bool(self.playlists)
|
||||
|
||||
if _is_cache_valid():
|
||||
@@ -133,7 +200,12 @@ class OasisCloudClient:
|
||||
return self.playlists
|
||||
|
||||
async def async_get_track_info(self, track_id: int) -> dict[str, Any] | None:
|
||||
"""Get single track info from the cloud."""
|
||||
"""
|
||||
Retrieve information for a single track from the cloud.
|
||||
|
||||
Returns:
|
||||
dict: Track detail dictionary. If the track is not found (HTTP 404), returns a dict with keys `id` and `name` where `name` is "Unknown Title (#{id})". Returns `None` on other failures.
|
||||
"""
|
||||
try:
|
||||
return await self._async_auth_request("GET", f"api/track/{track_id}")
|
||||
except ClientResponseError as err:
|
||||
@@ -146,7 +218,15 @@ class OasisCloudClient:
|
||||
async def async_get_tracks(
|
||||
self, tracks: list[int] | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get multiple tracks info from the cloud (handles pagination)."""
|
||||
"""
|
||||
Retrieve track details for the given track IDs, following pagination until all pages are fetched.
|
||||
|
||||
Parameters:
|
||||
tracks (list[int] | None): Optional list of track IDs to request. If omitted or None, an empty list is sent to the API.
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]: A list of track detail dictionaries returned by the cloud, aggregated across all pages (may be empty).
|
||||
"""
|
||||
response = await self._async_auth_request(
|
||||
"GET",
|
||||
"api/track",
|
||||
@@ -163,10 +243,24 @@ class OasisCloudClient:
|
||||
async def async_get_latest_software_details(
|
||||
self, *, force_refresh: bool = False
|
||||
) -> dict[str, int | str] | None:
|
||||
"""Get latest software metadata from cloud (cached)."""
|
||||
"""
|
||||
Retrieve the latest software metadata from the cloud, using an internal cache to limit requests.
|
||||
|
||||
Parameters:
|
||||
force_refresh (bool): If True, bypass the cache and fetch fresh metadata from the cloud.
|
||||
|
||||
Returns:
|
||||
details (dict[str, int | str] | None): A mapping of software metadata keys to integer or string values, or `None` if no metadata is available.
|
||||
"""
|
||||
now_dt = now()
|
||||
|
||||
def _is_cache_valid() -> bool:
|
||||
"""
|
||||
Determine whether the cached software metadata should be used instead of fetching fresh data.
|
||||
|
||||
Returns:
|
||||
True if the software cache exists, has not expired, and a force refresh was not requested; False otherwise.
|
||||
"""
|
||||
return (
|
||||
not force_refresh
|
||||
and self._software_details is not None
|
||||
@@ -193,7 +287,23 @@ class OasisCloudClient:
|
||||
return self._software_details
|
||||
|
||||
async def _async_auth_request(self, method: str, url: str, **kwargs: Any) -> Any:
|
||||
"""Perform an authenticated cloud request."""
|
||||
"""
|
||||
Perform a cloud API request using the stored access token.
|
||||
|
||||
If `url` is relative it will be joined with the module `BASE_URL`. The method will
|
||||
inject an `Authorization: Bearer <token>` header into the request.
|
||||
|
||||
Parameters:
|
||||
method (str): HTTP method (e.g., "GET", "POST").
|
||||
url (str): Absolute URL or path relative to `BASE_URL`.
|
||||
**kwargs: Passed through to the underlying request helper.
|
||||
|
||||
Returns:
|
||||
The parsed response value (JSON object, text, or None) as returned by the underlying request helper.
|
||||
|
||||
Raises:
|
||||
UnauthenticatedError: If no access token is set.
|
||||
"""
|
||||
if not self.access_token:
|
||||
raise UnauthenticatedError("Unauthenticated")
|
||||
|
||||
@@ -208,7 +318,30 @@ class OasisCloudClient:
|
||||
)
|
||||
|
||||
async def _async_request(self, method: str, url: str, **kwargs: Any) -> Any:
|
||||
"""Low-level HTTP helper for both cloud and (if desired) device HTTP."""
|
||||
"""
|
||||
Perform a single HTTP request and return a normalized response value.
|
||||
|
||||
Performs the request using the client's session and:
|
||||
- If the response status is 200:
|
||||
- returns parsed JSON for `application/json`.
|
||||
- returns text for `text/plain`.
|
||||
- if `text/html` and the URL targets the cloud base URL and contains a login page, raises UnauthenticatedError.
|
||||
- returns `None` for other content types.
|
||||
- If the response status is 401, raises UnauthenticatedError.
|
||||
- For other non-200 statuses, re-raises the response's HTTP error.
|
||||
|
||||
Parameters:
|
||||
method: HTTP method to use (e.g., "GET", "POST").
|
||||
url: Request URL or path.
|
||||
**kwargs: Passed through to the session request (e.g., `params`, `json`, `headers`).
|
||||
|
||||
Returns:
|
||||
The parsed JSON object, response text, or `None` depending on the response content type.
|
||||
|
||||
Raises:
|
||||
UnauthenticatedError: when the server indicates the client is unauthenticated (401) or a cloud login page is returned.
|
||||
aiohttp.ClientResponseError: for other non-success HTTP statuses raised by `response.raise_for_status()`.
|
||||
"""
|
||||
session = self.session
|
||||
_LOGGER.debug(
|
||||
"%s %s",
|
||||
@@ -233,4 +366,4 @@ class OasisCloudClient:
|
||||
if response.status == 401:
|
||||
raise UnauthenticatedError("Unauthenticated")
|
||||
|
||||
response.raise_for_status()
|
||||
response.raise_for_status()
|
||||
@@ -22,29 +22,64 @@ class OasisHttpClient(OasisClientProtocol):
|
||||
"""
|
||||
|
||||
def __init__(self, host: str, session: ClientSession | None = None) -> None:
|
||||
"""
|
||||
Initialize the HTTP client for a specific device host.
|
||||
|
||||
Parameters:
|
||||
host (str): Hostname or IP address of the target device (used to build the base HTTP URL).
|
||||
session (ClientSession | None): Optional aiohttp ClientSession to reuse for requests. If omitted, a new session will be created and owned by this client.
|
||||
"""
|
||||
self._host = host
|
||||
self._session: ClientSession | None = session
|
||||
self._owns_session: bool = session is None
|
||||
|
||||
@property
|
||||
def session(self) -> ClientSession:
|
||||
"""
|
||||
Ensure and return a usable aiohttp ClientSession for this client.
|
||||
|
||||
If no session exists or the existing session is closed, a new ClientSession is created and the client records ownership of that session so it can be closed later.
|
||||
|
||||
Returns:
|
||||
An active aiohttp ClientSession instance associated with this client.
|
||||
"""
|
||||
if self._session is None or self._session.closed:
|
||||
self._session = ClientSession()
|
||||
self._owns_session = True
|
||||
return self._session
|
||||
|
||||
async def async_close(self) -> None:
|
||||
"""Close owned session."""
|
||||
"""
|
||||
Close the client's owned HTTP session if one exists and is open.
|
||||
|
||||
Does nothing when there is no session, the session is already closed, or the client does not own the session.
|
||||
"""
|
||||
if self._session and not self._session.closed and self._owns_session:
|
||||
await self._session.close()
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
# These devices are plain HTTP, no TLS
|
||||
"""
|
||||
Base HTTP URL for the target device.
|
||||
|
||||
Returns:
|
||||
The device base URL using plain HTTP (no TLS), including a trailing slash (e.g. "http://{host}/").
|
||||
"""
|
||||
return f"http://{self._host}/"
|
||||
|
||||
async def _async_request(self, method: str, url: str, **kwargs: Any) -> Any:
|
||||
"""Low-level HTTP helper."""
|
||||
"""
|
||||
Perform an HTTP request using the client's session and decode the response.
|
||||
|
||||
Logs the request URL and query parameters. If the response status is 200, returns the response body as a string for `text/plain`, the parsed JSON for `application/json`, or `None` for other content types. On non-200 responses, raises the client response error.
|
||||
|
||||
Returns:
|
||||
The response body as `str` for `text/plain`, the parsed JSON value for `application/json`, or `None` for other content types.
|
||||
|
||||
Raises:
|
||||
aiohttp.ClientResponseError: If the response status is not 200.
|
||||
"""
|
||||
session = self.session
|
||||
_LOGGER.debug(
|
||||
"%s %s",
|
||||
@@ -65,15 +100,38 @@ class OasisHttpClient(OasisClientProtocol):
|
||||
resp.raise_for_status()
|
||||
|
||||
async def _async_get(self, **kwargs: Any) -> str | None:
|
||||
"""
|
||||
Perform a GET request to the client's base URL using the provided request keyword arguments.
|
||||
|
||||
Parameters:
|
||||
**kwargs: Additional request keyword arguments forwarded to the underlying request (for example `params`, `headers`, `timeout`).
|
||||
|
||||
Returns:
|
||||
`str` response text when the server responds with `text/plain`, `None` otherwise.
|
||||
"""
|
||||
return await self._async_request("GET", self.url, **kwargs)
|
||||
|
||||
async def _async_command(self, **kwargs: Any) -> str | None:
|
||||
"""
|
||||
Execute a device command by issuing a GET request with the provided query parameters and return the parsed response.
|
||||
|
||||
Parameters:
|
||||
**kwargs: Mapping of query parameter names to values sent with the GET request.
|
||||
|
||||
Returns:
|
||||
str | None: The device response as a string, a parsed JSON value, or None when the response has an unsupported content type.
|
||||
"""
|
||||
result = await self._async_get(**kwargs)
|
||||
_LOGGER.debug("Result: %s", result)
|
||||
return result
|
||||
|
||||
async def async_get_mac_address(self, device: OasisDevice) -> str | None:
|
||||
"""Fetch MAC address via HTTP GETMAC."""
|
||||
"""
|
||||
Fetch the device MAC address using the device's HTTP GETMAC endpoint.
|
||||
|
||||
Returns:
|
||||
str: The MAC address with surrounding whitespace removed, or `None` if it could not be retrieved.
|
||||
"""
|
||||
try:
|
||||
mac = await self._async_get(params={"GETMAC": ""})
|
||||
if isinstance(mac, str):
|
||||
@@ -89,6 +147,13 @@ class OasisHttpClient(OasisClientProtocol):
|
||||
device: OasisDevice,
|
||||
speed: int,
|
||||
) -> None:
|
||||
"""
|
||||
Send a ball speed command to the specified device.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device for the command.
|
||||
speed (int): Speed value to set for the device's ball mechanism.
|
||||
"""
|
||||
await self._async_command(params={"WRIOASISSPEED": speed})
|
||||
|
||||
async def async_send_led_command(
|
||||
@@ -99,10 +164,25 @@ class OasisHttpClient(OasisClientProtocol):
|
||||
led_speed: int,
|
||||
brightness: int,
|
||||
) -> None:
|
||||
"""
|
||||
Send an LED control command to the device.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device to receive the command.
|
||||
led_effect (str): Effect name or identifier to apply to the LEDs.
|
||||
color (str): Color value recognized by the device (e.g., hex code or device color name).
|
||||
led_speed (int): Animation speed value; larger values increase animation speed.
|
||||
brightness (int): Brightness level for the LEDs.
|
||||
"""
|
||||
payload = f"{led_effect};0;{color};{led_speed};{brightness}"
|
||||
await self._async_command(params={"WRILED": payload})
|
||||
|
||||
async def async_send_sleep_command(self, device: OasisDevice) -> None:
|
||||
"""
|
||||
Send a sleep command to the device.
|
||||
|
||||
Requests the device to enter sleep mode.
|
||||
"""
|
||||
await self._async_command(params={"CMDSLEEP": ""})
|
||||
|
||||
async def async_send_move_job_command(
|
||||
@@ -111,6 +191,14 @@ class OasisHttpClient(OasisClientProtocol):
|
||||
from_index: int,
|
||||
to_index: int,
|
||||
) -> None:
|
||||
"""
|
||||
Move a job in the device's playlist from one index to another.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device whose job list will be modified.
|
||||
from_index (int): Zero-based index of the job to move.
|
||||
to_index (int): Zero-based destination index where the job will be placed.
|
||||
"""
|
||||
await self._async_command(params={"MOVEJOB": f"{from_index};{to_index}"})
|
||||
|
||||
async def async_send_change_track_command(
|
||||
@@ -118,6 +206,12 @@ class OasisHttpClient(OasisClientProtocol):
|
||||
device: OasisDevice,
|
||||
index: int,
|
||||
) -> None:
|
||||
"""
|
||||
Change the device's current track to the specified track index.
|
||||
|
||||
Parameters:
|
||||
index (int): Zero-based index of the track to select.
|
||||
"""
|
||||
await self._async_command(params={"CMDCHANGETRACK": index})
|
||||
|
||||
async def async_send_add_joblist_command(
|
||||
@@ -126,6 +220,15 @@ class OasisHttpClient(OasisClientProtocol):
|
||||
tracks: list[int],
|
||||
) -> None:
|
||||
# The old code passed the list directly; if the device expects CSV:
|
||||
"""
|
||||
Send an "add joblist" command to the device with a list of track indices.
|
||||
|
||||
The provided track indices are serialized as a comma-separated string and sent to the device using the `ADDJOBLIST` parameter.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device to receive the command.
|
||||
tracks (list[int]): Track indices to add; these are sent as a CSV string (e.g., [1,2,3] -> "1,2,3").
|
||||
"""
|
||||
await self._async_command(params={"ADDJOBLIST": ",".join(map(str, tracks))})
|
||||
|
||||
async def async_send_set_playlist_command(
|
||||
@@ -133,6 +236,13 @@ class OasisHttpClient(OasisClientProtocol):
|
||||
device: OasisDevice,
|
||||
playlist: list[int],
|
||||
) -> None:
|
||||
"""
|
||||
Set the device's playlist on the target device and optimistically update the local device state.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device to receive the playlist command; its state will be updated optimistically.
|
||||
playlist (list[int]): Ordered list of track indices to set as the device's playlist.
|
||||
"""
|
||||
await self._async_command(params={"WRIJOBLIST": ",".join(map(str, playlist))})
|
||||
# optional: optimistic state update
|
||||
device.update_from_status_dict({"playlist": playlist})
|
||||
@@ -142,6 +252,12 @@ class OasisHttpClient(OasisClientProtocol):
|
||||
device: OasisDevice,
|
||||
repeat: bool,
|
||||
) -> None:
|
||||
"""
|
||||
Set the device's playlist repeat flag.
|
||||
|
||||
Parameters:
|
||||
repeat (bool): `True` to enable playlist repeat, `False` to disable it.
|
||||
"""
|
||||
await self._async_command(params={"WRIREPEATJOB": 1 if repeat else 0})
|
||||
|
||||
async def async_send_set_autoplay_command(
|
||||
@@ -149,6 +265,13 @@ class OasisHttpClient(OasisClientProtocol):
|
||||
device: OasisDevice,
|
||||
option: str,
|
||||
) -> None:
|
||||
"""
|
||||
Set the device's autoplay (wait-after) option.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device whose autoplay option will be updated.
|
||||
option (str): The value for the device's wait-after/autoplay setting as expected by the device firmware.
|
||||
"""
|
||||
await self._async_command(params={"WRIWAITAFTER": option})
|
||||
|
||||
async def async_send_upgrade_command(
|
||||
@@ -156,25 +279,55 @@ class OasisHttpClient(OasisClientProtocol):
|
||||
device: OasisDevice,
|
||||
beta: bool,
|
||||
) -> None:
|
||||
"""
|
||||
Send a firmware upgrade command to the specified device.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device to receive the upgrade command.
|
||||
beta (bool): If True, request the beta firmware; if False, request the stable firmware.
|
||||
"""
|
||||
await self._async_command(params={"CMDUPGRADE": 1 if beta else 0})
|
||||
|
||||
async def async_send_play_command(self, device: OasisDevice) -> None:
|
||||
"""
|
||||
Send the play command to the device.
|
||||
"""
|
||||
await self._async_command(params={"CMDPLAY": ""})
|
||||
|
||||
async def async_send_pause_command(self, device: OasisDevice) -> None:
|
||||
"""
|
||||
Send a pause command to the device.
|
||||
"""
|
||||
await self._async_command(params={"CMDPAUSE": ""})
|
||||
|
||||
async def async_send_stop_command(self, device: OasisDevice) -> None:
|
||||
"""
|
||||
Sends the device stop command to halt playback or activity.
|
||||
|
||||
Sends an HTTP command to request the device stop its current operation.
|
||||
"""
|
||||
await self._async_command(params={"CMDSTOP": ""})
|
||||
|
||||
async def async_send_reboot_command(self, device: OasisDevice) -> None:
|
||||
"""
|
||||
Send a reboot command to the device.
|
||||
|
||||
Sends a reboot request to the target device using the CMDBOOT control parameter.
|
||||
"""
|
||||
await self._async_command(params={"CMDBOOT": ""})
|
||||
|
||||
async def async_get_status(self, device: OasisDevice) -> None:
|
||||
"""Fetch status via GETSTATUS and update the device."""
|
||||
"""
|
||||
Retrieve the device status from the device and apply it to the given OasisDevice.
|
||||
|
||||
If the device does not return a status, the device object is not modified.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Device instance to update with the fetched status.
|
||||
"""
|
||||
raw_status = await self._async_get(params={"GETSTATUS": ""})
|
||||
if raw_status is None:
|
||||
return
|
||||
|
||||
_LOGGER.debug("Status for %s: %s", device.serial_number, raw_status)
|
||||
device.update_from_status_string(raw_status)
|
||||
device.update_from_status_string(raw_status)
|
||||
@@ -42,6 +42,24 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
|
||||
def __init__(self) -> None:
|
||||
# MQTT connection state
|
||||
"""
|
||||
Initialize internal state for the MQTT transport client.
|
||||
|
||||
Sets up connection state, per-device registries and events, subscription bookkeeping, and a bounded pending command queue capped by MAX_PENDING_COMMANDS.
|
||||
|
||||
Attributes:
|
||||
_client: Active aiomqtt client or None.
|
||||
_loop_task: Background MQTT loop task or None.
|
||||
_connected_at: Timestamp of last successful connection or None.
|
||||
_connected_event: Event signaled when a connection is established.
|
||||
_stop_event: Event signaled to request the loop to stop.
|
||||
_devices: Mapping of device serial to OasisDevice instances.
|
||||
_first_status_events: Per-serial events signaled on receiving the first STATUS message.
|
||||
_mac_events: Per-serial events signaled when a device MAC address is received.
|
||||
_subscribed_serials: Set of serials currently subscribed to STATUS topics.
|
||||
_subscription_lock: Lock protecting subscribe/unsubscribe operations.
|
||||
_command_queue: Bounded queue of pending (serial, payload) commands.
|
||||
"""
|
||||
self._client: aiomqtt.Client | None = None
|
||||
self._loop_task: asyncio.Task | None = None
|
||||
self._connected_at: datetime | None = None
|
||||
@@ -66,7 +84,17 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
)
|
||||
|
||||
def register_device(self, device: OasisDevice) -> None:
|
||||
"""Register a device so MQTT messages can be routed to it."""
|
||||
"""
|
||||
Register an OasisDevice so MQTT messages for its serial are routed to that device.
|
||||
|
||||
Ensures the device has a serial_number (raises ValueError if not), stores the device in the client's registry, creates per-device asyncio.Events for first-status and MAC-address arrival, attaches this client to the device if it has no client, and schedules a subscription for the device's STATUS topics if the MQTT client is currently connected.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): The device instance to register.
|
||||
|
||||
Raises:
|
||||
ValueError: If `device.serial_number` is not set.
|
||||
"""
|
||||
if not device.serial_number:
|
||||
raise ValueError("Device must have serial_number set before registration")
|
||||
|
||||
@@ -93,11 +121,24 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
)
|
||||
|
||||
def register_devices(self, devices: Iterable[OasisDevice]) -> None:
|
||||
"""Convenience method to register multiple devices."""
|
||||
"""
|
||||
Register multiple OasisDevice instances with the client.
|
||||
|
||||
Parameters:
|
||||
devices (Iterable[OasisDevice]): Iterable of devices to register.
|
||||
"""
|
||||
for device in devices:
|
||||
self.register_device(device)
|
||||
|
||||
def unregister_device(self, device: OasisDevice) -> None:
|
||||
"""
|
||||
Unregisters a device from MQTT routing and cleans up related per-device state.
|
||||
|
||||
Removes the device's registration, first-status and MAC events. If there is an active MQTT client and the device's serial is currently subscribed, schedules an asynchronous unsubscription task. If the device has no serial_number, the call is a no-op.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): The device to unregister; must have `serial_number` set.
|
||||
"""
|
||||
serial = device.serial_number
|
||||
if not serial:
|
||||
return
|
||||
@@ -118,7 +159,11 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
)
|
||||
|
||||
async def _subscribe_serial(self, serial: str) -> None:
|
||||
"""Subscribe to STATUS topics for a single device."""
|
||||
"""
|
||||
Subscribe to the device's STATUS topic pattern and mark the device as subscribed.
|
||||
|
||||
Subscribes to "<serial>/STATUS/#" with QoS 1 and records the subscription; does nothing if the MQTT client is not connected or the serial is already subscribed.
|
||||
"""
|
||||
if not self._client:
|
||||
return
|
||||
|
||||
@@ -132,7 +177,13 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
_LOGGER.info("Subscribed to %s", topic)
|
||||
|
||||
async def _unsubscribe_serial(self, serial: str) -> None:
|
||||
"""Unsubscribe from STATUS topics for a single device."""
|
||||
"""
|
||||
Unsubscribe from the device's STATUS topic and update subscription state.
|
||||
|
||||
If there is no active MQTT client or the serial is not currently subscribed, this is a no-op.
|
||||
Parameters:
|
||||
serial (str): Device serial used to build the topic "<serial>/STATUS/#".
|
||||
"""
|
||||
if not self._client:
|
||||
return
|
||||
|
||||
@@ -164,7 +215,11 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
await self.stop()
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop MQTT connection loop."""
|
||||
"""
|
||||
Stop the MQTT client and clean up resources.
|
||||
|
||||
Signals the background MQTT loop to stop, cancels the loop task, disconnects the MQTT client if connected, and clears any pending commands from the internal command queue.
|
||||
"""
|
||||
self._stop_event.set()
|
||||
|
||||
if self._loop_task:
|
||||
@@ -194,11 +249,20 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
self, device: OasisDevice, timeout: float = 10.0, request_status: bool = True
|
||||
) -> bool:
|
||||
"""
|
||||
Wait until:
|
||||
1. MQTT client is connected
|
||||
2. Device sends at least one STATUS message
|
||||
|
||||
If request_status=True, a request status command is sent *after* connection.
|
||||
Block until the MQTT client is connected and the device has emitted at least one STATUS message.
|
||||
|
||||
If `request_status` is True, a status request is sent after the client is connected to prompt the device to report its state.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): The device to wait for; must have `serial_number` set.
|
||||
timeout (float): Maximum seconds to wait for connection and for the first STATUS message.
|
||||
request_status (bool): If True, issue a status refresh after connection to encourage a STATUS update.
|
||||
|
||||
Returns:
|
||||
bool: `True` if the device's first STATUS message was observed within the timeout, `False` otherwise.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the provided device does not have a `serial_number`.
|
||||
"""
|
||||
serial = device.serial_number
|
||||
if not serial:
|
||||
@@ -243,7 +307,20 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
return False
|
||||
|
||||
async def async_get_mac_address(self, device: OasisDevice) -> str | None:
|
||||
"""For MQTT, GETSTATUS causes MAC_ADDRESS to be published."""
|
||||
"""
|
||||
Request a device's MAC address via an MQTT STATUS refresh and return it if available.
|
||||
|
||||
If the device already has a MAC address, it is returned immediately. Otherwise the function requests a status update (which causes the device to publish MAC_ADDRESS) and waits up to 3 seconds for the MAC to arrive.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): The device whose MAC address will be requested.
|
||||
|
||||
Returns:
|
||||
str | None: The device MAC address if obtained, `None` if the wait timed out and no MAC was received.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the provided device has no serial_number set.
|
||||
"""
|
||||
# If already known on the device, return it
|
||||
if device.mac_address:
|
||||
return device.mac_address
|
||||
@@ -268,7 +345,13 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
async def async_send_auto_clean_command(
|
||||
self, device: OasisDevice, auto_clean: bool
|
||||
) -> None:
|
||||
"""Send auto clean command."""
|
||||
"""
|
||||
Set the device's automatic cleaning mode.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target Oasis device to send the command to.
|
||||
auto_clean (bool): True to enable automatic cleaning, False to disable.
|
||||
"""
|
||||
payload = f"WRIAUTOCLEAN={1 if auto_clean else 0}"
|
||||
await self._publish_command(device, payload)
|
||||
|
||||
@@ -277,6 +360,13 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
device: OasisDevice,
|
||||
speed: int,
|
||||
) -> None:
|
||||
"""
|
||||
Set the device's ball speed.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device.
|
||||
speed (int): Speed value to apply.
|
||||
"""
|
||||
payload = f"WRIOASISSPEED={speed}"
|
||||
await self._publish_command(device, payload)
|
||||
|
||||
@@ -288,10 +378,28 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
led_speed: int,
|
||||
brightness: int,
|
||||
) -> None:
|
||||
"""
|
||||
Send an LED configuration command to the device.
|
||||
|
||||
If `brightness` is greater than zero, the device is woken before sending the command.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device (must have a serial number).
|
||||
led_effect (str): LED effect identifier to apply.
|
||||
color (str): Color value for the LED effect (format expected by device).
|
||||
led_speed (int): Speed/tempo for the LED effect.
|
||||
brightness (int): Brightness level to set; also used to determine wake behavior.
|
||||
"""
|
||||
payload = f"WRILED={led_effect};0;{color};{led_speed};{brightness}"
|
||||
await self._publish_command(device, payload, bool(brightness))
|
||||
|
||||
async def async_send_sleep_command(self, device: OasisDevice) -> None:
|
||||
"""
|
||||
Send the sleep command to the specified Oasis device.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device; must have a valid serial_number. If the MQTT client is not connected, the command may be queued for delivery when a connection is available.
|
||||
"""
|
||||
await self._publish_command(device, "CMDSLEEP")
|
||||
|
||||
async def async_send_move_job_command(
|
||||
@@ -300,6 +408,14 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
from_index: int,
|
||||
to_index: int,
|
||||
) -> None:
|
||||
"""
|
||||
Move a job in the device's playlist from one index to another.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device to receive the command.
|
||||
from_index (int): Source index of the job in the playlist.
|
||||
to_index (int): Destination index where the job should be placed.
|
||||
"""
|
||||
payload = f"MOVEJOB={from_index};{to_index}"
|
||||
await self._publish_command(device, payload)
|
||||
|
||||
@@ -308,6 +424,13 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
device: OasisDevice,
|
||||
index: int,
|
||||
) -> None:
|
||||
"""
|
||||
Change the device's current track to the specified track index.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target Oasis device.
|
||||
index (int): Track index to switch to (zero-based).
|
||||
"""
|
||||
payload = f"CMDCHANGETRACK={index}"
|
||||
await self._publish_command(device, payload)
|
||||
|
||||
@@ -316,6 +439,13 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
device: OasisDevice,
|
||||
tracks: list[int],
|
||||
) -> None:
|
||||
"""
|
||||
Send an ADDJOBLIST command to add multiple tracks to the device's job list.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device to receive the command.
|
||||
tracks (list[int]): List of track indices to add; elements will be joined as a comma-separated list in the command payload.
|
||||
"""
|
||||
track_str = ",".join(map(str, tracks))
|
||||
payload = f"ADDJOBLIST={track_str}"
|
||||
await self._publish_command(device, payload)
|
||||
@@ -325,6 +455,13 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
device: OasisDevice,
|
||||
playlist: list[int],
|
||||
) -> None:
|
||||
"""
|
||||
Set the device's playlist to the specified ordered list of track indices.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target Oasis device to receive the playlist command.
|
||||
playlist (list[int]): Ordered list of track indices to apply as the device's playlist.
|
||||
"""
|
||||
track_str = ",".join(map(str, playlist))
|
||||
payload = f"WRIJOBLIST={track_str}"
|
||||
await self._publish_command(device, payload)
|
||||
@@ -334,6 +471,13 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
device: OasisDevice,
|
||||
repeat: bool,
|
||||
) -> None:
|
||||
"""
|
||||
Send a command to enable or disable repeating the device's playlist.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device; must have a serial number.
|
||||
repeat (bool): True to enable playlist repeat, False to disable it.
|
||||
"""
|
||||
payload = f"WRIREPEATJOB={1 if repeat else 0}"
|
||||
await self._publish_command(device, payload)
|
||||
|
||||
@@ -342,6 +486,15 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
device: OasisDevice,
|
||||
option: str,
|
||||
) -> None:
|
||||
"""
|
||||
Set the device's wait-after-job / autoplay option.
|
||||
|
||||
Publishes a "WRIWAITAFTER=<option>" command for the specified device to configure how long the device waits after a job or to adjust autoplay behavior.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device (must have a serial_number).
|
||||
option (str): Value accepted by the device firmware for the wait-after-job/autoplay setting (typically a numeric string or predefined option token).
|
||||
"""
|
||||
payload = f"WRIWAITAFTER={option}"
|
||||
await self._publish_command(device, payload)
|
||||
|
||||
@@ -350,19 +503,48 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
device: OasisDevice,
|
||||
beta: bool,
|
||||
) -> None:
|
||||
"""
|
||||
Request a firmware upgrade for the given device.
|
||||
|
||||
Sends an upgrade command to the device and selects the beta channel when requested.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device.
|
||||
beta (bool): If `True`, request a beta firmware upgrade; if `False`, request the stable firmware.
|
||||
"""
|
||||
payload = f"CMDUPGRADE={1 if beta else 0}"
|
||||
await self._publish_command(device, payload)
|
||||
|
||||
async def async_send_play_command(self, device: OasisDevice) -> None:
|
||||
"""
|
||||
Send a "play" command to the given device and wake it if the device is sleeping.
|
||||
"""
|
||||
await self._publish_command(device, "CMDPLAY", True)
|
||||
|
||||
async def async_send_pause_command(self, device: OasisDevice) -> None:
|
||||
"""
|
||||
Sends a pause command to the specified Oasis device.
|
||||
|
||||
Publishes the "CMDPAUSE" command to the device's command topic.
|
||||
"""
|
||||
await self._publish_command(device, "CMDPAUSE")
|
||||
|
||||
async def async_send_stop_command(self, device: OasisDevice) -> None:
|
||||
"""
|
||||
Send the "stop" command to the given Oasis device.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device to receive the stop command; must be registered with a valid serial number.
|
||||
"""
|
||||
await self._publish_command(device, "CMDSTOP")
|
||||
|
||||
async def async_send_reboot_command(self, device: OasisDevice) -> None:
|
||||
"""
|
||||
Send a reboot command to the specified Oasis device.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device to receive the reboot command; must have a valid serial_number.
|
||||
"""
|
||||
await self._publish_command(device, "CMDBOOT")
|
||||
|
||||
async def async_get_all(self, device: OasisDevice) -> None:
|
||||
@@ -370,7 +552,9 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
await self._publish_command(device, "GETALL")
|
||||
|
||||
async def async_get_status(self, device: OasisDevice) -> None:
|
||||
"""Ask device to publish STATUS topics."""
|
||||
"""
|
||||
Request the device to publish its current STATUS topics.
|
||||
"""
|
||||
await self._publish_command(device, "GETSTATUS")
|
||||
|
||||
async def _enqueue_command(self, serial: str, payload: str) -> None:
|
||||
@@ -393,7 +577,11 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
_LOGGER.debug("Queued command for %s: %s", serial, payload)
|
||||
|
||||
async def _flush_pending_commands(self) -> None:
|
||||
"""Send any queued commands now that we're connected."""
|
||||
"""
|
||||
Flush queued commands by publishing them to each device's COMMAND/CMD topic.
|
||||
|
||||
This consumes all entries from the internal command queue, skipping entries for devices that are no longer registered, publishing each payload to "<serial>/COMMAND/CMD" with QoS 1, and marking queue tasks done. If a publish fails, the failed command is re-queued and flushing stops so remaining queued commands will be retried on the next reconnect.
|
||||
"""
|
||||
if not self._client:
|
||||
return
|
||||
|
||||
@@ -431,6 +619,19 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
async def _publish_command(
|
||||
self, device: OasisDevice, payload: str, wake: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
Publish a command payload to the device's MQTT COMMAND topic, queueing it if the client is not connected.
|
||||
|
||||
If `wake` is True and the device reports it is sleeping, requests a full status refresh before publishing. If the MQTT client is not connected or publish fails, the command is enqueued for later delivery.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device; must have a valid `serial_number`.
|
||||
payload (str): Command payload to send to the device.
|
||||
wake (bool): If True, refresh the device state when the device is sleeping before sending the command.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the provided device has no serial number set.
|
||||
"""
|
||||
serial = device.serial_number
|
||||
if not serial:
|
||||
raise RuntimeError("Device has no serial number set")
|
||||
@@ -457,6 +658,11 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
await self._enqueue_command(serial, payload)
|
||||
|
||||
async def _mqtt_loop(self) -> None:
|
||||
"""
|
||||
Run the MQTT WebSocket connection loop that maintains connection, subscriptions, and message handling.
|
||||
|
||||
This background coroutine establishes a persistent WSS MQTT connection to the configured broker, sets connection state on successful connect, resubscribes to known device STATUS topics, flushes any queued outbound commands, and dispatches incoming MQTT messages to the status handler. On disconnect or error it clears connection state and subscription tracking, and retries connecting after the configured backoff interval until the client is stopped.
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
tls_context = await loop.run_in_executor(None, ssl.create_default_context)
|
||||
|
||||
@@ -514,7 +720,18 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
await asyncio.sleep(RECONNECT_INTERVAL)
|
||||
|
||||
async def _handle_status_message(self, msg: aiomqtt.Message) -> None:
|
||||
"""Map MQTT STATUS topics → OasisDevice.update_from_status_dict payloads."""
|
||||
"""
|
||||
Map an incoming MQTT STATUS message to an OasisDevice state update.
|
||||
|
||||
Expects msg.topic in the form "<serial>/STATUS/<STATUS_NAME>" and decodes msg.payload as text.
|
||||
If the topic corresponds to a registered device, extracts the relevant status field and calls
|
||||
the device's update_from_status_dict with a mapping of the parsed values. For the "MAC_ADDRESS"
|
||||
status, sets the per-device MAC event to signal arrival of the MAC address. Always sets the
|
||||
per-device first-status event once any status is processed for that serial.
|
||||
|
||||
Parameters:
|
||||
msg (aiomqtt.Message): Incoming MQTT message; topic identifies device serial and status.
|
||||
"""
|
||||
topic_str = str(msg.topic) if msg.topic is not None else ""
|
||||
payload = msg.payload.decode(errors="replace")
|
||||
|
||||
@@ -614,4 +831,4 @@ class OasisMqttClient(OasisClientProtocol):
|
||||
serial, asyncio.Event()
|
||||
)
|
||||
if not first_status_event.is_set():
|
||||
first_status_event.set()
|
||||
first_status_event.set()
|
||||
@@ -14,17 +14,40 @@ class OasisClientProtocol(Protocol):
|
||||
- HTTP client (direct LAN)
|
||||
"""
|
||||
|
||||
async def async_get_mac_address(self, device: OasisDevice) -> str | None: ...
|
||||
async def async_get_mac_address(self, device: OasisDevice) -> str | None: """
|
||||
Retrieve the MAC address of the specified Oasis device.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): The target device to query.
|
||||
|
||||
Returns:
|
||||
str | None: The device's MAC address as a string, or `None` if the MAC address is unavailable.
|
||||
"""
|
||||
...
|
||||
|
||||
async def async_send_auto_clean_command(
|
||||
self, device: OasisDevice, auto_clean: bool
|
||||
) -> None: ...
|
||||
) -> None: """
|
||||
Enable or disable the device's auto-clean mode.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): The target Oasis device to send the command to.
|
||||
auto_clean (bool): `True` to enable auto-clean mode, `False` to disable it.
|
||||
"""
|
||||
...
|
||||
|
||||
async def async_send_ball_speed_command(
|
||||
self,
|
||||
device: OasisDevice,
|
||||
speed: int,
|
||||
) -> None: ...
|
||||
) -> None: """
|
||||
Set the device's ball speed to the specified value.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target Oasis device to send the command to.
|
||||
speed (int): Desired ball speed value for the device.
|
||||
"""
|
||||
...
|
||||
|
||||
async def async_send_led_command(
|
||||
self,
|
||||
@@ -33,61 +56,167 @@ class OasisClientProtocol(Protocol):
|
||||
color: str,
|
||||
led_speed: int,
|
||||
brightness: int,
|
||||
) -> None: ...
|
||||
) -> None: """
|
||||
Configure the device's LED effect, color, speed, and brightness.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target Oasis device to receive the LED command.
|
||||
led_effect (str): Name or identifier of the LED effect to apply.
|
||||
color (str): Color for the LED effect (format depends on implementation, e.g., hex code or color name).
|
||||
led_speed (int): Effect speed; larger values increase the animation speed.
|
||||
brightness (int): Brightness level as a percentage from 0 to 100.
|
||||
"""
|
||||
...
|
||||
|
||||
async def async_send_sleep_command(self, device: OasisDevice) -> None: ...
|
||||
async def async_send_sleep_command(self, device: OasisDevice) -> None: """
|
||||
Put the specified Oasis device into sleep mode.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): The target Oasis device to send the sleep command to.
|
||||
"""
|
||||
...
|
||||
|
||||
async def async_send_move_job_command(
|
||||
self,
|
||||
device: OasisDevice,
|
||||
from_index: int,
|
||||
to_index: int,
|
||||
) -> None: ...
|
||||
) -> None: """
|
||||
Move a job within the device's job list from one index to another.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target Oasis device.
|
||||
from_index (int): Source index of the job in the device's job list.
|
||||
to_index (int): Destination index to move the job to.
|
||||
"""
|
||||
...
|
||||
|
||||
async def async_send_change_track_command(
|
||||
self,
|
||||
device: OasisDevice,
|
||||
index: int,
|
||||
) -> None: ...
|
||||
) -> None: """
|
||||
Change the device's current track to the specified track index.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): The target Oasis device to receive the command.
|
||||
index (int): The index of the track to select on the device.
|
||||
"""
|
||||
...
|
||||
|
||||
async def async_send_add_joblist_command(
|
||||
self,
|
||||
device: OasisDevice,
|
||||
tracks: list[int],
|
||||
) -> None: ...
|
||||
) -> None: """
|
||||
Add the given sequence of track indices to the device's job list.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target Oasis device to receive the new jobs.
|
||||
tracks (list[int]): Ordered list of track indices to append to the device's job list.
|
||||
"""
|
||||
...
|
||||
|
||||
async def async_send_set_playlist_command(
|
||||
self,
|
||||
device: OasisDevice,
|
||||
playlist: list[int],
|
||||
) -> None: ...
|
||||
) -> None: """
|
||||
Set the device's current playlist to the provided sequence of track indices.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): The target Oasis device to receive the playlist.
|
||||
playlist (list[int]): Sequence of track indices in the desired playback order.
|
||||
"""
|
||||
...
|
||||
|
||||
async def async_send_set_repeat_playlist_command(
|
||||
self,
|
||||
device: OasisDevice,
|
||||
repeat: bool,
|
||||
) -> None: ...
|
||||
) -> None: """
|
||||
Set whether the device should repeat the current playlist.
|
||||
|
||||
Parameters:
|
||||
repeat (bool): True to enable repeating the current playlist, False to disable it.
|
||||
"""
|
||||
...
|
||||
|
||||
async def async_send_set_autoplay_command(
|
||||
self,
|
||||
device: OasisDevice,
|
||||
option: str,
|
||||
) -> None: ...
|
||||
) -> None: """
|
||||
Send a command to configure the device's autoplay behavior.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target Oasis device to receive the command.
|
||||
option (str): Autoplay option to set (e.g., "on", "off", "shuffle", or other device-supported mode).
|
||||
"""
|
||||
...
|
||||
|
||||
async def async_send_upgrade_command(
|
||||
self,
|
||||
device: OasisDevice,
|
||||
beta: bool,
|
||||
) -> None: ...
|
||||
) -> None: """
|
||||
Initiates a firmware upgrade on the given Oasis device.
|
||||
|
||||
If `beta` is True, requests the device to use the beta upgrade channel; otherwise requests the stable channel.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device to upgrade.
|
||||
beta (bool): Whether to use the beta upgrade channel (`True`) or the stable channel (`False`).
|
||||
"""
|
||||
...
|
||||
|
||||
async def async_send_play_command(self, device: OasisDevice) -> None: ...
|
||||
async def async_send_play_command(self, device: OasisDevice) -> None: """
|
||||
Send a play command to the specified Oasis device.
|
||||
|
||||
async def async_send_pause_command(self, device: OasisDevice) -> None: ...
|
||||
Parameters:
|
||||
device (OasisDevice): The target device to instruct to start playback.
|
||||
"""
|
||||
...
|
||||
|
||||
async def async_send_stop_command(self, device: OasisDevice) -> None: ...
|
||||
async def async_send_pause_command(self, device: OasisDevice) -> None: """
|
||||
Pause playback on the specified Oasis device.
|
||||
|
||||
async def async_send_reboot_command(self, device: OasisDevice) -> None: ...
|
||||
This sends a pause command to the device so it stops current playback.
|
||||
"""
|
||||
...
|
||||
|
||||
async def async_get_all(self, device: OasisDevice) -> None: ...
|
||||
async def async_send_stop_command(self, device: OasisDevice) -> None: """
|
||||
Send a stop command to the specified Oasis device to halt playback.
|
||||
|
||||
async def async_get_status(self, device: OasisDevice) -> None: ...
|
||||
Parameters:
|
||||
device (OasisDevice): The target Oasis device to receive the stop command.
|
||||
"""
|
||||
...
|
||||
|
||||
async def async_send_reboot_command(self, device: OasisDevice) -> None: """
|
||||
Send a reboot command to the specified Oasis device.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): The target Oasis device to reboot.
|
||||
"""
|
||||
...
|
||||
|
||||
async def async_get_all(self, device: OasisDevice) -> None: """
|
||||
Fetch comprehensive device data for the specified Oasis device.
|
||||
|
||||
This method triggers retrieval of all relevant information (configuration, status, and runtime data) for the given device so the client's representation of that device can be refreshed.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): Target device whose data should be fetched and refreshed.
|
||||
"""
|
||||
...
|
||||
|
||||
async def async_get_status(self, device: OasisDevice) -> None: """
|
||||
Retrieve the current runtime status for the specified Oasis device.
|
||||
|
||||
Implementations should query the device for its current state (for example: playback, LED settings, job/track lists, and connectivity) and update any client-side representation or caches as needed.
|
||||
|
||||
Parameters:
|
||||
device (OasisDevice): The target device to query.
|
||||
"""
|
||||
...
|
||||
@@ -70,6 +70,23 @@ class OasisDevice:
|
||||
client: OasisClientProtocol | None = None,
|
||||
) -> None:
|
||||
# Transport
|
||||
"""
|
||||
Initialize an OasisDevice with identification, network, transport references, and default state fields.
|
||||
|
||||
Parameters:
|
||||
model (str | None): Device model identifier.
|
||||
serial_number (str | None): Device serial number.
|
||||
name (str | None): Human-readable device name; if omitted, defaults to "<model> <serial_number>".
|
||||
ssid (str | None): Last-known Wi‑Fi SSID for the device.
|
||||
ip_address (str | None): Last-known IP address for the device.
|
||||
cloud (OasisCloudClient | None): Optional cloud client used to fetch track metadata and remote data.
|
||||
client (OasisClientProtocol | None): Optional transport client used to send commands to the device.
|
||||
|
||||
Notes:
|
||||
- Creates an internal listener list for state-change callbacks.
|
||||
- Initializes status fields (brightness, playlist, playback state, networking, etc.) with sensible defaults.
|
||||
- Initializes a track metadata cache and a placeholder for a background refresh task.
|
||||
"""
|
||||
self._cloud = cloud
|
||||
self._client = client
|
||||
self._listeners: list[Callable[[], None]] = []
|
||||
@@ -118,18 +135,34 @@ class OasisDevice:
|
||||
|
||||
@property
|
||||
def brightness(self) -> int:
|
||||
"""Return the brightness."""
|
||||
"""
|
||||
Current display brightness adjusted for the device sleep state.
|
||||
|
||||
Returns:
|
||||
int: 0 when the device is sleeping, otherwise the stored brightness value.
|
||||
"""
|
||||
return 0 if self.is_sleeping else self._brightness
|
||||
|
||||
@brightness.setter
|
||||
def brightness(self, value: int) -> None:
|
||||
"""
|
||||
Set the device brightness and update brightness_on when non-zero.
|
||||
|
||||
Parameters:
|
||||
value (int): Brightness level to apply; if non-zero, also stored in `brightness_on`.
|
||||
"""
|
||||
self._brightness = value
|
||||
if value:
|
||||
self.brightness_on = value
|
||||
|
||||
@property
|
||||
def is_sleeping(self) -> bool:
|
||||
"""Return `True` if the status is set to sleeping."""
|
||||
"""
|
||||
Indicates whether the device is currently in the sleeping status.
|
||||
|
||||
Returns:
|
||||
`true` if the device is sleeping, `false` otherwise.
|
||||
"""
|
||||
return self.status_code == STATUS_CODE_SLEEPING
|
||||
|
||||
def attach_client(self, client: OasisClientProtocol) -> None:
|
||||
@@ -138,11 +171,24 @@ class OasisDevice:
|
||||
|
||||
@property
|
||||
def client(self) -> OasisClientProtocol | None:
|
||||
"""Return the current transport client, if any."""
|
||||
"""
|
||||
Get the attached transport client, or `None` if no client is attached.
|
||||
|
||||
Returns:
|
||||
The attached transport client, or `None` if not attached.
|
||||
"""
|
||||
return self._client
|
||||
|
||||
def _require_client(self) -> OasisClientProtocol:
|
||||
"""Return the attached client or raise if missing."""
|
||||
"""
|
||||
Get the attached transport client for this device.
|
||||
|
||||
Returns:
|
||||
OasisClientProtocol: The attached transport client.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If no client/transport is attached to the device.
|
||||
"""
|
||||
if self._client is None:
|
||||
raise RuntimeError(
|
||||
f"No client/transport attached for device {self.serial_number!r}"
|
||||
@@ -150,6 +196,18 @@ class OasisDevice:
|
||||
return self._client
|
||||
|
||||
def _update_field(self, name: str, value: Any) -> bool:
|
||||
"""
|
||||
Update an attribute on the device if the new value differs from the current value.
|
||||
|
||||
Sets the instance attribute named `name` to `value` and logs a debug message when a change occurs.
|
||||
|
||||
Parameters:
|
||||
name (str): The attribute name to update.
|
||||
value (Any): The new value to assign to the attribute.
|
||||
|
||||
Returns:
|
||||
bool: True if the attribute was changed, False otherwise.
|
||||
"""
|
||||
old = getattr(self, name, None)
|
||||
if old != value:
|
||||
_LOGGER.debug(
|
||||
@@ -164,7 +222,13 @@ class OasisDevice:
|
||||
return False
|
||||
|
||||
def update_from_status_dict(self, data: dict[str, Any]) -> None:
|
||||
"""Update device fields from a status payload (from any transport)."""
|
||||
"""
|
||||
Update the device's attributes from a status dictionary.
|
||||
|
||||
Expects a mapping of attribute names to values; known attributes are applied to the device,
|
||||
unknown keys are logged and ignored. If `playlist` or `playlist_index` change, a track
|
||||
refresh is scheduled. If any attribute changed, registered update listeners are notified.
|
||||
"""
|
||||
changed = False
|
||||
playlist_or_index_changed = False
|
||||
|
||||
@@ -184,11 +248,22 @@ class OasisDevice:
|
||||
self._notify_listeners()
|
||||
|
||||
def parse_status_string(self, raw_status: str) -> dict[str, Any] | None:
|
||||
"""Parse a semicolon-separated status string into a state dict.
|
||||
|
||||
Used by:
|
||||
- HTTP GETSTATUS response
|
||||
- MQTT FULLSTATUS payload (includes software_version)
|
||||
"""
|
||||
Parse a semicolon-separated device status string into a structured state dictionary.
|
||||
|
||||
Expects a semicolon-separated string containing at least 18 fields (device status format returned by the device: e.g., HTTP GETSTATUS or MQTT FULLSTATUS). Returns None for empty input or if the string cannot be parsed into the expected fields.
|
||||
|
||||
Parameters:
|
||||
raw_status (str): Semicolon-separated status string from the device.
|
||||
|
||||
Returns:
|
||||
dict[str, Any] | None: A dictionary with these keys on success:
|
||||
- `status_code`, `error`, `ball_speed`, `playlist` (list[int]), `playlist_index`,
|
||||
`progress`, `led_effect`, `led_color_id`, `led_speed`, `brightness`, `color`,
|
||||
`busy`, `download_progress`, `brightness_max`, `wifi_connected`, `repeat_playlist`,
|
||||
`autoplay`, `auto_clean`
|
||||
- `software_version` (str) is included if an additional trailing field is present.
|
||||
Returns `None` if the input is empty or parsing fails.
|
||||
"""
|
||||
if not raw_status:
|
||||
return None
|
||||
@@ -239,35 +314,71 @@ class OasisDevice:
|
||||
return status
|
||||
|
||||
def update_from_status_string(self, raw_status: str) -> None:
|
||||
"""Parse and apply a raw status string."""
|
||||
"""
|
||||
Parse a semicolon-separated device status string and apply the resulting fields to the device state.
|
||||
|
||||
If the string cannot be parsed into a valid status dictionary, no state is changed.
|
||||
|
||||
Parameters:
|
||||
raw_status (str): Raw status payload received from the device (semicolon-separated fields).
|
||||
"""
|
||||
if status := self.parse_status_string(raw_status):
|
||||
self.update_from_status_dict(status)
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return core state as a dict."""
|
||||
"""
|
||||
Return a mapping of the device's core state fields to their current values.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: A dictionary whose keys are the core state field names (as defined in _STATE_FIELDS)
|
||||
and whose values are the current values for those fields.
|
||||
"""
|
||||
return {field: getattr(self, field) for field in _STATE_FIELDS}
|
||||
|
||||
@property
|
||||
def error_message(self) -> str | None:
|
||||
"""Return the error message, if any."""
|
||||
"""
|
||||
Get the human-readable error message for the current device error code.
|
||||
|
||||
Returns:
|
||||
str: The mapped error message when the device status indicates an error (status code 9); `None` otherwise.
|
||||
"""
|
||||
if self.status_code == 9:
|
||||
return ERROR_CODE_MAP.get(self.error, f"Unknown ({self.error})")
|
||||
return None
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
"""Return human-readable status from status_code."""
|
||||
"""
|
||||
Get a human-readable status description for the current status_code.
|
||||
|
||||
Returns:
|
||||
str: Human-readable status corresponding to the device's status_code, or "Unknown (<code>)" when the code is not recognized.
|
||||
"""
|
||||
return STATUS_CODE_MAP.get(self.status_code, f"Unknown ({self.status_code})")
|
||||
|
||||
@property
|
||||
def track(self) -> dict | None:
|
||||
"""Return cached track info if it matches the current `track_id`."""
|
||||
"""
|
||||
Return the cached track metadata when it corresponds to the current track, otherwise retrieve the built-in track metadata.
|
||||
|
||||
Returns:
|
||||
dict | None: The track metadata dictionary for the current `track_id`, or `None` if no matching track is available.
|
||||
"""
|
||||
if (track := self._track) and track["id"] == self.track_id:
|
||||
return track
|
||||
return TRACKS.get(self.track_id)
|
||||
|
||||
@property
|
||||
def track_id(self) -> int | None:
|
||||
"""
|
||||
Determine the current track id from the active playlist.
|
||||
|
||||
If the playlist index is beyond the end of the playlist, the first track id is returned.
|
||||
|
||||
Returns:
|
||||
int | None: The current track id, or `None` if there is no playlist.
|
||||
"""
|
||||
if not self.playlist:
|
||||
return None
|
||||
i = self.playlist_index
|
||||
@@ -275,21 +386,40 @@ class OasisDevice:
|
||||
|
||||
@property
|
||||
def track_image_url(self) -> str | None:
|
||||
"""Return the track image url, if any."""
|
||||
"""
|
||||
Get the full HTTPS URL for the current track's image if available.
|
||||
|
||||
Returns:
|
||||
str: Full URL to the track image (https://app.grounded.so/uploads/<image>), or `None` if no image is available.
|
||||
"""
|
||||
if (track := self.track) and (image := track.get("image")):
|
||||
return f"https://app.grounded.so/uploads/{image}"
|
||||
return None
|
||||
|
||||
@property
|
||||
def track_name(self) -> str | None:
|
||||
"""Return the track name, if any."""
|
||||
"""
|
||||
Get the current track's display name.
|
||||
|
||||
If the current track has no explicit name, returns "Unknown Title (#{track_id})". If there is no current track, returns None.
|
||||
|
||||
Returns:
|
||||
str | None: The track name, or `None` if no current track is available.
|
||||
"""
|
||||
if track := self.track:
|
||||
return track.get("name", f"Unknown Title (#{self.track_id})")
|
||||
return None
|
||||
|
||||
@property
|
||||
def drawing_progress(self) -> float | None:
|
||||
"""Return drawing progress percentage for the current track."""
|
||||
"""
|
||||
Compute drawing progress percentage for the current track.
|
||||
|
||||
If the current track or its SVG content is unavailable, returns None.
|
||||
|
||||
Returns:
|
||||
progress_percent (float | None): Percentage of the drawing completed (0–100), clamped to 100; `None` if no track or SVG content is available.
|
||||
"""
|
||||
if not (self.track and (svg_content := self.track.get("svg_content"))):
|
||||
return None
|
||||
svg_content = decrypt_svg_content(svg_content)
|
||||
@@ -300,7 +430,12 @@ class OasisDevice:
|
||||
|
||||
@property
|
||||
def playlist_details(self) -> dict[int, dict[str, str]]:
|
||||
"""Basic playlist details using built-in TRACKS metadata."""
|
||||
"""
|
||||
Build a mapping of track IDs in the current playlist to their detail dictionaries, preferring the device's cached/current track data and falling back to built-in TRACKS.
|
||||
|
||||
Returns:
|
||||
dict[int, dict[str, str]]: A mapping from track ID to a details dictionary (contains at least a `'name'` key). If track metadata is available from the device cache or built-in TRACKS it is used; otherwise a fallback `{"name": "Unknown Title (#<id>)"}` is provided.
|
||||
"""
|
||||
return {
|
||||
track_id: {self.track_id: self.track or {}, **TRACKS}.get(
|
||||
track_id,
|
||||
@@ -310,17 +445,32 @@ class OasisDevice:
|
||||
}
|
||||
|
||||
def create_svg(self) -> str | None:
|
||||
"""Create the current svg based on track and progress."""
|
||||
"""
|
||||
Generate an SVG representing the current track at the device's drawing progress.
|
||||
|
||||
Returns:
|
||||
svg (str | None): SVG content for the current track reflecting current progress, or None if track data is unavailable.
|
||||
"""
|
||||
return create_svg(self.track, self.progress)
|
||||
|
||||
def add_update_listener(self, listener: Callable[[], None]) -> Callable[[], None]:
|
||||
"""Register a callback for state changes.
|
||||
|
||||
Returns an unsubscribe function.
|
||||
"""
|
||||
Register a callback to be invoked whenever the device state changes.
|
||||
|
||||
Parameters:
|
||||
listener (Callable[[], None]): A zero-argument callback that will be called on state updates.
|
||||
|
||||
Returns:
|
||||
Callable[[], None]: An unsubscribe function that removes the registered listener; calling the unsubscribe function multiple times is safe.
|
||||
"""
|
||||
self._listeners.append(listener)
|
||||
|
||||
def _unsub() -> None:
|
||||
"""
|
||||
Remove the previously registered listener from the device's listener list if it is present.
|
||||
|
||||
This function silently does nothing if the listener is not found.
|
||||
"""
|
||||
try:
|
||||
self._listeners.remove(listener)
|
||||
except ValueError:
|
||||
@@ -329,7 +479,11 @@ class OasisDevice:
|
||||
return _unsub
|
||||
|
||||
def _notify_listeners(self) -> None:
|
||||
"""Call all registered listeners."""
|
||||
"""
|
||||
Invoke all registered update listeners in registration order.
|
||||
|
||||
Each listener is called synchronously; exceptions raised by a listener are caught and logged so other listeners still run.
|
||||
"""
|
||||
for listener in list(self._listeners):
|
||||
try:
|
||||
listener()
|
||||
@@ -337,7 +491,12 @@ class OasisDevice:
|
||||
_LOGGER.exception("Error in update listener")
|
||||
|
||||
async def async_get_mac_address(self) -> str | None:
|
||||
"""Return the device MAC address, refreshing via transport if needed."""
|
||||
"""
|
||||
Get the device MAC address, requesting it from the attached transport client if not already known.
|
||||
|
||||
Returns:
|
||||
mac (str | None): The device MAC address if available, otherwise `None`.
|
||||
"""
|
||||
if self.mac_address:
|
||||
return self.mac_address
|
||||
|
||||
@@ -348,10 +507,25 @@ class OasisDevice:
|
||||
return mac
|
||||
|
||||
async def async_set_auto_clean(self, auto_clean: bool) -> None:
|
||||
"""
|
||||
Set whether the device performs automatic cleaning.
|
||||
|
||||
Parameters:
|
||||
auto_clean (bool): `True` to enable automatic cleaning, `False` to disable it.
|
||||
"""
|
||||
client = self._require_client()
|
||||
await client.async_send_auto_clean_command(self, auto_clean)
|
||||
|
||||
async def async_set_ball_speed(self, speed: int) -> None:
|
||||
"""
|
||||
Set the device's ball speed.
|
||||
|
||||
Parameters:
|
||||
speed (int): Desired ball speed in the allowed range (BALL_SPEED_MIN to BALL_SPEED_MAX, inclusive).
|
||||
|
||||
Raises:
|
||||
ValueError: If `speed` is outside the allowed range.
|
||||
"""
|
||||
if not BALL_SPEED_MIN <= speed <= BALL_SPEED_MAX:
|
||||
raise ValueError("Invalid speed specified")
|
||||
client = self._require_client()
|
||||
@@ -365,7 +539,19 @@ class OasisDevice:
|
||||
led_speed: int | None = None,
|
||||
brightness: int | None = None,
|
||||
) -> None:
|
||||
"""Set the Oasis device LED (shared validation & attribute updates)."""
|
||||
"""
|
||||
Set the device LED effect, color, speed, and brightness.
|
||||
|
||||
Parameters:
|
||||
led_effect (str | None): LED effect name; if None, the device's current effect is used. Must be one of the supported LED effects.
|
||||
color (str | None): Hex color string (e.g. "#rrggbb"); if None, the device's current color is used or `#ffffff` if unset.
|
||||
led_speed (int | None): LED animation speed; if None, the device's current speed is used. Must be within the allowed LED speed range.
|
||||
brightness (int | None): Brightness level; if None, the device's current brightness is used. Must be between 0 and the device's `brightness_max`.
|
||||
|
||||
Raises:
|
||||
ValueError: If `led_effect` is not supported, or `led_speed` or `brightness` are outside their valid ranges.
|
||||
RuntimeError: If no transport client is attached to the device.
|
||||
"""
|
||||
if led_effect is None:
|
||||
led_effect = self.led_effect
|
||||
if color is None:
|
||||
@@ -388,18 +574,48 @@ class OasisDevice:
|
||||
)
|
||||
|
||||
async def async_sleep(self) -> None:
|
||||
"""
|
||||
Put the device into sleep mode.
|
||||
|
||||
Sends a sleep command to the attached transport client.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If no client is attached.
|
||||
"""
|
||||
client = self._require_client()
|
||||
await client.async_send_sleep_command(self)
|
||||
|
||||
async def async_move_track(self, from_index: int, to_index: int) -> None:
|
||||
"""
|
||||
Move a track within the device's playlist from one index to another.
|
||||
|
||||
Parameters:
|
||||
from_index (int): Index of the track to move within the current playlist.
|
||||
to_index (int): Destination index where the track should be placed.
|
||||
"""
|
||||
client = self._require_client()
|
||||
await client.async_send_move_job_command(self, from_index, to_index)
|
||||
|
||||
async def async_change_track(self, index: int) -> None:
|
||||
"""
|
||||
Change the device's current track to the track at the given playlist index.
|
||||
|
||||
Parameters:
|
||||
index (int): Zero-based index of the track in the device's current playlist.
|
||||
"""
|
||||
client = self._require_client()
|
||||
await client.async_send_change_track_command(self, index)
|
||||
|
||||
async def async_add_track_to_playlist(self, track: int | Iterable[int]) -> None:
|
||||
"""
|
||||
Add one or more tracks to the device's playlist via the attached client.
|
||||
|
||||
Parameters:
|
||||
track (int | Iterable[int]): A single track id or an iterable of track ids to append to the playlist.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If no transport client is attached to the device.
|
||||
"""
|
||||
if isinstance(track, int):
|
||||
tracks = [track]
|
||||
else:
|
||||
@@ -408,6 +624,14 @@ class OasisDevice:
|
||||
await client.async_send_add_joblist_command(self, tracks)
|
||||
|
||||
async def async_set_playlist(self, playlist: int | Iterable[int]) -> 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.
|
||||
|
||||
Parameters:
|
||||
playlist (int | Iterable[int]): A single track ID or an iterable of track IDs to set as the new playlist.
|
||||
"""
|
||||
if isinstance(playlist, int):
|
||||
playlist_list = [playlist]
|
||||
else:
|
||||
@@ -416,38 +640,85 @@ class OasisDevice:
|
||||
await client.async_send_set_playlist_command(self, playlist_list)
|
||||
|
||||
async def async_set_repeat_playlist(self, repeat: bool) -> None:
|
||||
"""
|
||||
Set whether the device's playlist should repeat.
|
||||
|
||||
Parameters:
|
||||
repeat (bool): True to enable repeating the playlist, False to disable it.
|
||||
"""
|
||||
client = self._require_client()
|
||||
await client.async_send_set_repeat_playlist_command(self, repeat)
|
||||
|
||||
async def async_set_autoplay(self, option: bool | int | str) -> None:
|
||||
"""Set autoplay / wait-after behavior."""
|
||||
"""
|
||||
Set the device's autoplay / wait-after option.
|
||||
|
||||
Parameters:
|
||||
option (bool | int | str): Desired autoplay/wait-after value. If a `bool` is provided, `True` is converted to `"0"` and `False` to `"1"`. Integer or string values are sent as their string representation.
|
||||
"""
|
||||
if isinstance(option, bool):
|
||||
option = 0 if option else 1
|
||||
client = self._require_client()
|
||||
await client.async_send_set_autoplay_command(self, str(option))
|
||||
|
||||
async def async_upgrade(self, beta: bool = False) -> None:
|
||||
"""
|
||||
Initiates a firmware upgrade on the device.
|
||||
|
||||
Parameters:
|
||||
beta (bool): If True, request a beta (pre-release) firmware; otherwise request the stable firmware.
|
||||
"""
|
||||
client = self._require_client()
|
||||
await client.async_send_upgrade_command(self, beta)
|
||||
|
||||
async def async_play(self) -> None:
|
||||
"""
|
||||
Send a play command to the device via the attached transport client.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If no transport client is attached.
|
||||
"""
|
||||
client = self._require_client()
|
||||
await client.async_send_play_command(self)
|
||||
|
||||
async def async_pause(self) -> None:
|
||||
"""
|
||||
Pause playback on the device.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If no transport client is attached.
|
||||
"""
|
||||
client = self._require_client()
|
||||
await client.async_send_pause_command(self)
|
||||
|
||||
async def async_stop(self) -> None:
|
||||
"""
|
||||
Stop playback on the device by sending a stop command through the attached transport client.
|
||||
|
||||
Raises:
|
||||
RuntimeError: if no transport client is attached to the device.
|
||||
"""
|
||||
client = self._require_client()
|
||||
await client.async_send_stop_command(self)
|
||||
|
||||
async def async_reboot(self) -> None:
|
||||
"""
|
||||
Reboots the device using the attached transport client.
|
||||
|
||||
Requests the attached client to send a reboot command to the device.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If no transport client is attached.
|
||||
"""
|
||||
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."""
|
||||
"""
|
||||
Schedule a background refresh of the current track metadata when the device's track may have changed.
|
||||
|
||||
Does nothing if no cloud client is attached or if there is no running event loop. If a previous refresh task is still pending, it is cancelled before a new background task is scheduled.
|
||||
"""
|
||||
if not self._cloud:
|
||||
return
|
||||
|
||||
@@ -463,7 +734,11 @@ class OasisDevice:
|
||||
self._track_task = loop.create_task(self._async_refresh_current_track())
|
||||
|
||||
async def _async_refresh_current_track(self) -> None:
|
||||
"""Refresh the current track info."""
|
||||
"""
|
||||
Refresh cached information for the current track by fetching details from the attached cloud client and notify listeners when updated.
|
||||
|
||||
If no cloud client is attached, no current track exists, or the cached track already matches the current track id, the method returns without change. On successful fetch, updates the device's track cache and invokes registered update listeners.
|
||||
"""
|
||||
if not self._cloud:
|
||||
return
|
||||
|
||||
@@ -484,4 +759,4 @@ class OasisDevice:
|
||||
return
|
||||
|
||||
self._track = track
|
||||
self._notify_listeners()
|
||||
self._notify_listeners()
|
||||
@@ -28,7 +28,15 @@ def _bit_to_bool(val: str) -> bool:
|
||||
|
||||
|
||||
def _parse_int(val: str) -> int:
|
||||
"""Convert an int string to int."""
|
||||
"""
|
||||
Parse a string into an integer, falling back to 0 when conversion fails.
|
||||
|
||||
Parameters:
|
||||
val (str): String potentially containing an integer value.
|
||||
|
||||
Returns:
|
||||
int: The parsed integer, or 0 if `val` cannot be converted.
|
||||
"""
|
||||
try:
|
||||
return int(val)
|
||||
except Exception:
|
||||
@@ -36,7 +44,18 @@ def _parse_int(val: str) -> int:
|
||||
|
||||
|
||||
def create_svg(track: dict, progress: int) -> str | None:
|
||||
"""Create an SVG from a track based on progress."""
|
||||
"""
|
||||
Create an SVG visualization of a track showing progress as a completed path and indicator.
|
||||
|
||||
Builds an SVG representation from the track's "svg_content" and the provided progress value. If progress is supplied, the function will decrypt the stored SVG content (if needed), compute which path segments are complete using the track's optional "reduced_svg_content_new" value or the number of path segments, and render a base arc, completed arc, track, completed track segment, background circle, and a ball indicator positioned at the current progress point. Returns None if input is missing or an error occurs.
|
||||
|
||||
Parameters:
|
||||
track (dict): Track data containing at minimum an "svg_content" entry and optionally "reduced_svg_content_new" to indicate total segments.
|
||||
progress (int): Current progress expressed as a count relative to the track's total segments.
|
||||
|
||||
Returns:
|
||||
str | None: Serialized SVG markup as a UTF-8 string when successful, otherwise `None`.
|
||||
"""
|
||||
if track and (svg_content := track.get("svg_content")):
|
||||
try:
|
||||
if progress is not None:
|
||||
@@ -181,4 +200,4 @@ def decrypt_svg_content(svg_content: dict[str, str]):
|
||||
|
||||
|
||||
def now() -> datetime:
|
||||
return datetime.now(UTC)
|
||||
return datetime.now(UTC)
|
||||
Reference in New Issue
Block a user