mirror of
https://github.com/natekspencer/hacs-oasis_mini.git
synced 2025-12-07 02:54:12 -05:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b32199c334 | ||
|
|
5c49119ae5 | ||
|
|
fbb3012379 | ||
|
|
ac005c70c2 | ||
|
|
873d2d4bb0 | ||
|
|
04be6626a7 | ||
|
|
14223bd1c9 | ||
|
|
1d521bcc18 | ||
|
|
2994e73187 | ||
|
|
e4f6cd2803 | ||
|
|
1cc3585653 | ||
|
|
2f28f7c4bd | ||
|
|
81668c595a | ||
|
|
c17d1682d0 | ||
|
|
f0669c7f63 | ||
|
|
8abfc047f9 | ||
|
|
0df118d18d | ||
|
|
0ebab392fb | ||
|
|
a15548e387 | ||
|
|
b459e3eb9d | ||
|
|
a6ecd740be | ||
|
|
aa7abc2174 | ||
|
|
a548ac5fe2 | ||
|
|
ce22238ae6 | ||
|
|
4ef28fc741 | ||
|
|
cf21a5d995 | ||
|
|
83de1d5606 | ||
|
|
2a92212aad | ||
|
|
ecad472bbd | ||
|
|
886d7598f3 |
2
.github/workflows/update-tracks.yml
vendored
2
.github/workflows/update-tracks.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
python-version: "3.13"
|
python-version: "3.13"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pip install homeassistant aiomqtt
|
run: pip install homeassistant
|
||||||
|
|
||||||
- name: Update tracks
|
- name: Update tracks
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from .const import DOMAIN
|
|||||||
from .coordinator import OasisDeviceCoordinator
|
from .coordinator import OasisDeviceCoordinator
|
||||||
from .entity import OasisDeviceEntity
|
from .entity import OasisDeviceEntity
|
||||||
from .helpers import create_client
|
from .helpers import create_client
|
||||||
from .pyoasiscontrol import OasisDevice, UnauthenticatedError
|
from .pyoasiscontrol import OasisDevice, OasisMqttClient, UnauthenticatedError
|
||||||
|
|
||||||
type OasisDeviceConfigEntry = ConfigEntry[OasisDeviceCoordinator]
|
type OasisDeviceConfigEntry = ConfigEntry[OasisDeviceCoordinator]
|
||||||
|
|
||||||
@@ -94,9 +94,7 @@ def setup_platform_from_coordinator(
|
|||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: OasisDeviceConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: OasisDeviceConfigEntry) -> bool:
|
||||||
"""
|
"""
|
||||||
Initialize Oasis cloud for a config entry, create and refresh the device
|
Initialize Oasis cloud and MQTT integration for a config entry, create and refresh the device coordinator, register update listeners for discovered devices, forward platform setup, and update the entry's metadata as needed.
|
||||||
coordinator, register update listeners for discovered devices, forward platform
|
|
||||||
setup, and update the entry's metadata as needed.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if the config entry was set up successfully.
|
True if the config entry was set up successfully.
|
||||||
@@ -111,12 +109,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: OasisDeviceConfigEntry)
|
|||||||
await cloud_client.async_close()
|
await cloud_client.async_close()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
coordinator = OasisDeviceCoordinator(hass, entry, cloud_client)
|
mqtt_client = OasisMqttClient()
|
||||||
|
coordinator = OasisDeviceCoordinator(hass, entry, cloud_client, mqtt_client)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
mqtt_client.start()
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
except Exception:
|
except Exception:
|
||||||
await coordinator.async_close()
|
await mqtt_client.async_close()
|
||||||
|
await cloud_client.async_close()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if entry.unique_id != (user_id := str(user["id"])):
|
if entry.unique_id != (user_id := str(user["id"])):
|
||||||
@@ -150,17 +151,18 @@ async def async_unload_entry(
|
|||||||
"""
|
"""
|
||||||
Cleanly unload an Oasis device config entry.
|
Cleanly unload an Oasis device config entry.
|
||||||
|
|
||||||
Unloads all supported platforms and closes the coordinator connections.
|
Closes the MQTT and cloud clients stored on the entry and unloads all supported platforms.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
`True` if all platforms were unloaded successfully, `False` otherwise.
|
`True` if all platforms were unloaded successfully, `False` otherwise.
|
||||||
"""
|
"""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
mqtt_client = entry.runtime_data.mqtt_client
|
||||||
try:
|
await mqtt_client.async_close()
|
||||||
await entry.runtime_data.async_close()
|
|
||||||
except Exception:
|
cloud_client = entry.runtime_data.cloud_client
|
||||||
_LOGGER.exception("Error closing Oasis coordinator during unload")
|
await cloud_client.async_close()
|
||||||
return unload_ok
|
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
async def async_remove_entry(
|
async def async_remove_entry(
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import async_timeout
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
@@ -32,6 +33,7 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]):
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: OasisDeviceConfigEntry,
|
config_entry: OasisDeviceConfigEntry,
|
||||||
cloud_client: OasisCloudClient,
|
cloud_client: OasisCloudClient,
|
||||||
|
mqtt_client: OasisMqttClient,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Create an OasisDeviceCoordinator that manages OasisDevice discovery and updates using cloud and MQTT clients.
|
Create an OasisDeviceCoordinator that manages OasisDevice discovery and updates using cloud and MQTT clients.
|
||||||
@@ -39,6 +41,7 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]):
|
|||||||
Parameters:
|
Parameters:
|
||||||
config_entry (OasisDeviceConfigEntry): The config entry whose runtime data contains device serial numbers.
|
config_entry (OasisDeviceConfigEntry): The config entry whose runtime data contains device serial numbers.
|
||||||
cloud_client (OasisCloudClient): Client for communicating with the Oasis cloud API and fetching device data.
|
cloud_client (OasisCloudClient): Client for communicating with the Oasis cloud API and fetching device data.
|
||||||
|
mqtt_client (OasisMqttClient): Client for registering devices and coordinating MQTT-based readiness/status.
|
||||||
"""
|
"""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
@@ -49,7 +52,7 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]):
|
|||||||
always_update=False,
|
always_update=False,
|
||||||
)
|
)
|
||||||
self.cloud_client = cloud_client
|
self.cloud_client = cloud_client
|
||||||
self.mqtt_client = OasisMqttClient()
|
self.mqtt_client = mqtt_client
|
||||||
|
|
||||||
async def _async_update_data(self) -> list[OasisDevice]:
|
async def _async_update_data(self) -> list[OasisDevice]:
|
||||||
"""
|
"""
|
||||||
@@ -65,7 +68,7 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]):
|
|||||||
self.attempt += 1
|
self.attempt += 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with asyncio.timeout(30):
|
async with async_timeout.timeout(30):
|
||||||
raw_devices = await self.cloud_client.async_get_devices()
|
raw_devices = await self.cloud_client.async_get_devices()
|
||||||
|
|
||||||
existing_by_serial = {
|
existing_by_serial = {
|
||||||
@@ -108,19 +111,14 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]):
|
|||||||
remove_config_entry_id=self.config_entry.entry_id,
|
remove_config_entry_id=self.config_entry.entry_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# If logged in, but no devices on account, return without starting mqtt
|
# ✅ Valid state: logged in but no devices on account
|
||||||
if not devices:
|
if not devices:
|
||||||
_LOGGER.debug("No Oasis devices found for account")
|
_LOGGER.debug("No Oasis devices found for account")
|
||||||
if self.mqtt_client.is_running:
|
|
||||||
# Close the mqtt client if it was previously started
|
|
||||||
await self.mqtt_client.async_close()
|
|
||||||
self.attempt = 0
|
self.attempt = 0
|
||||||
if devices != self.data:
|
if devices != self.data:
|
||||||
self.last_updated = dt_util.now()
|
self.last_updated = dt_util.now()
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if not self.mqtt_client.is_running:
|
|
||||||
self.mqtt_client.start()
|
|
||||||
self.mqtt_client.register_devices(devices)
|
self.mqtt_client.register_devices(devices)
|
||||||
|
|
||||||
# Best-effort playlists
|
# Best-effort playlists
|
||||||
@@ -185,11 +183,3 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]):
|
|||||||
self.last_updated = dt_util.now()
|
self.last_updated = dt_util.now()
|
||||||
|
|
||||||
return devices
|
return devices
|
||||||
|
|
||||||
async def async_close(self) -> None:
|
|
||||||
"""Close client connections."""
|
|
||||||
await asyncio.gather(
|
|
||||||
self.mqtt_client.async_close(),
|
|
||||||
self.cloud_client.async_close(),
|
|
||||||
return_exceptions=True,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import async_timeout
|
||||||
|
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .pyoasiscontrol import OasisCloudClient, OasisDevice
|
from .pyoasiscontrol import OasisCloudClient, OasisDevice
|
||||||
from .pyoasiscontrol.const import STATUS_PLAYING, TRACKS
|
from .pyoasiscontrol.const import TRACKS
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -42,26 +44,24 @@ async def add_and_play_track(device: OasisDevice, track: int) -> None:
|
|||||||
track (int): The track id to add and play.
|
track (int): The track id to add and play.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
TimeoutError: If the operation does not complete within 10 seconds.
|
async_timeout.TimeoutError: If the operation does not complete within 10 seconds.
|
||||||
"""
|
"""
|
||||||
async with asyncio.timeout(10):
|
async with async_timeout.timeout(10):
|
||||||
if track not in device.playlist:
|
if track not in device.playlist:
|
||||||
await device.async_add_track_to_playlist(track)
|
await device.async_add_track_to_playlist(track)
|
||||||
|
|
||||||
# Wait for device state to reflect the newly added track
|
|
||||||
while track not in device.playlist:
|
while track not in device.playlist:
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
# Ensure the track is positioned immediately after the current track and select it
|
# Move track to next item in the playlist and then select it
|
||||||
if (index := device.playlist.index(track)) != device.playlist_index:
|
if (index := device.playlist.index(track)) != device.playlist_index:
|
||||||
# Calculate the position after the current track
|
|
||||||
if index != (
|
if index != (
|
||||||
_next := min(device.playlist_index + 1, len(device.playlist) - 1)
|
_next := min(device.playlist_index + 1, len(device.playlist) - 1)
|
||||||
):
|
):
|
||||||
await device.async_move_track(index, _next)
|
await device.async_move_track(index, _next)
|
||||||
await device.async_change_track(_next)
|
await device.async_change_track(_next)
|
||||||
|
|
||||||
if device.status_code != STATUS_PLAYING:
|
if device.status_code != 4:
|
||||||
await device.async_play()
|
await device.async_play()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -121,15 +121,13 @@ class OasisDeviceLightEntity(OasisDeviceEntity, LightEntity):
|
|||||||
"""
|
"""
|
||||||
Turn the light on and set its LED state.
|
Turn the light on and set its LED state.
|
||||||
|
|
||||||
Processes optional keyword arguments to compute the device-specific LED
|
Processes optional keyword arguments to compute the device-specific LED parameters, then updates the device's LEDs with the resulting brightness, color, and effect.
|
||||||
parameters, then updates the device's LEDs with the resulting brightness, color,
|
|
||||||
and effect.
|
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
kwargs: Optional control parameters recognized by the method:
|
kwargs: Optional control parameters recognized by the method:
|
||||||
ATTR_BRIGHTNESS (int): Brightness in the 0-255 Home Assistant scale. When provided,
|
ATTR_BRIGHTNESS (int): Brightness in the 0-255 Home Assistant scale. When provided,
|
||||||
it is converted and rounded up to the device's brightness scale (1..device.brightness_max).
|
it is converted and rounded up to the device's brightness scale (1..device.brightness_max).
|
||||||
When omitted, uses self.device.brightness_on (last non-zero brightness).
|
When omitted, uses self.device.brightness or self.device.brightness_on.
|
||||||
ATTR_RGB_COLOR (tuple[int, int, int]): RGB tuple (R, G, B). When provided, it is
|
ATTR_RGB_COLOR (tuple[int, int, int]): RGB tuple (R, G, B). When provided, it is
|
||||||
converted to a hex color string prefixed with '#'.
|
converted to a hex color string prefixed with '#'.
|
||||||
ATTR_EFFECT (str): Human-readable effect name. When provided, it is mapped to the
|
ATTR_EFFECT (str): Human-readable effect name. When provided, it is mapped to the
|
||||||
@@ -142,7 +140,7 @@ class OasisDeviceLightEntity(OasisDeviceEntity, LightEntity):
|
|||||||
scale = (1, self.device.brightness_max)
|
scale = (1, self.device.brightness_max)
|
||||||
brightness = math.ceil(brightness_to_value(scale, brightness))
|
brightness = math.ceil(brightness_to_value(scale, brightness))
|
||||||
else:
|
else:
|
||||||
brightness = self.device.brightness_on
|
brightness = self.device.brightness or self.device.brightness_on
|
||||||
|
|
||||||
if color := kwargs.get(ATTR_RGB_COLOR):
|
if color := kwargs.get(ATTR_RGB_COLOR):
|
||||||
color = f"#{color_rgb_to_hex(*color)}"
|
color = f"#{color_rgb_to_hex(*color)}"
|
||||||
|
|||||||
@@ -23,16 +23,6 @@ from .const import DOMAIN
|
|||||||
from .entity import OasisDeviceEntity
|
from .entity import OasisDeviceEntity
|
||||||
from .helpers import get_track_id
|
from .helpers import get_track_id
|
||||||
from .pyoasiscontrol import OasisDevice
|
from .pyoasiscontrol import OasisDevice
|
||||||
from .pyoasiscontrol.const import (
|
|
||||||
STATUS_CENTERING,
|
|
||||||
STATUS_DOWNLOADING,
|
|
||||||
STATUS_ERROR,
|
|
||||||
STATUS_LIVE,
|
|
||||||
STATUS_PAUSED,
|
|
||||||
STATUS_PLAYING,
|
|
||||||
STATUS_STOPPED,
|
|
||||||
STATUS_UPDATING,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -140,17 +130,17 @@ class OasisDeviceMediaPlayerEntity(OasisDeviceEntity, MediaPlayerEntity):
|
|||||||
def state(self) -> MediaPlayerState:
|
def state(self) -> MediaPlayerState:
|
||||||
"""State of the player."""
|
"""State of the player."""
|
||||||
status_code = self.device.status_code
|
status_code = self.device.status_code
|
||||||
if self.device.error or status_code in (STATUS_ERROR, STATUS_UPDATING):
|
if self.device.error or status_code in (9, 11):
|
||||||
return MediaPlayerState.OFF
|
return MediaPlayerState.OFF
|
||||||
if status_code == STATUS_STOPPED:
|
if status_code == 2:
|
||||||
return MediaPlayerState.IDLE
|
return MediaPlayerState.IDLE
|
||||||
if status_code in (STATUS_CENTERING, STATUS_DOWNLOADING):
|
if status_code in (3, 13):
|
||||||
return MediaPlayerState.BUFFERING
|
return MediaPlayerState.BUFFERING
|
||||||
if status_code == STATUS_PLAYING:
|
if status_code == 4:
|
||||||
return MediaPlayerState.PLAYING
|
return MediaPlayerState.PLAYING
|
||||||
if status_code == STATUS_PAUSED:
|
if status_code == 5:
|
||||||
return MediaPlayerState.PAUSED
|
return MediaPlayerState.PAUSED
|
||||||
if status_code == STATUS_LIVE:
|
if status_code == 15:
|
||||||
return MediaPlayerState.ON
|
return MediaPlayerState.ON
|
||||||
return MediaPlayerState.IDLE
|
return MediaPlayerState.IDLE
|
||||||
|
|
||||||
|
|||||||
@@ -83,20 +83,6 @@ class OasisMqttClient(OasisClientProtocol):
|
|||||||
maxsize=MAX_PENDING_COMMANDS
|
maxsize=MAX_PENDING_COMMANDS
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def is_running(self) -> bool:
|
|
||||||
"""Return `True` if the MQTT loop has been started and is not stopped."""
|
|
||||||
return (
|
|
||||||
self._loop_task is not None
|
|
||||||
and not self._loop_task.done()
|
|
||||||
and not self._stop_event.is_set()
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_connected(self) -> bool:
|
|
||||||
"""Return `True` if the MQTT client is currently connected."""
|
|
||||||
return self._connected_event.is_set()
|
|
||||||
|
|
||||||
def register_device(self, device: OasisDevice) -> None:
|
def register_device(self, device: OasisDevice) -> None:
|
||||||
"""
|
"""
|
||||||
Register an OasisDevice so MQTT messages for its serial are routed to that device.
|
Register an OasisDevice so MQTT messages for its serial are routed to that device.
|
||||||
@@ -232,49 +218,32 @@ class OasisMqttClient(OasisClientProtocol):
|
|||||||
"""
|
"""
|
||||||
Stop the MQTT client and clean up resources.
|
Stop the MQTT client and clean up resources.
|
||||||
|
|
||||||
Signals the background MQTT loop to stop, cancels the loop task,
|
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.
|
||||||
disconnects the MQTT client if connected, and drops any pending commands.
|
|
||||||
"""
|
"""
|
||||||
_LOGGER.debug("MQTT stop() called - beginning shutdown sequence")
|
|
||||||
self._stop_event.set()
|
self._stop_event.set()
|
||||||
|
|
||||||
if self._loop_task:
|
if self._loop_task:
|
||||||
_LOGGER.debug(
|
|
||||||
"Cancelling MQTT background task (task=%s, done=%s)",
|
|
||||||
self._loop_task,
|
|
||||||
self._loop_task.done(),
|
|
||||||
)
|
|
||||||
self._loop_task.cancel()
|
self._loop_task.cancel()
|
||||||
try:
|
try:
|
||||||
await self._loop_task
|
await self._loop_task
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
_LOGGER.debug("MQTT background task cancelled")
|
|
||||||
|
|
||||||
if self._client:
|
if self._client:
|
||||||
_LOGGER.debug("Disconnecting MQTT client from broker")
|
|
||||||
try:
|
try:
|
||||||
await self._client.disconnect()
|
await self._client.disconnect()
|
||||||
_LOGGER.debug("MQTT client disconnected")
|
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("Error disconnecting MQTT client")
|
_LOGGER.exception("Error disconnecting MQTT client")
|
||||||
finally:
|
finally:
|
||||||
self._client = None
|
self._client = None
|
||||||
|
|
||||||
# Drop queued commands
|
# Drop pending commands on stop
|
||||||
if not self._command_queue.empty():
|
|
||||||
_LOGGER.debug("Dropping queued commands")
|
|
||||||
dropped = 0
|
|
||||||
while not self._command_queue.empty():
|
while not self._command_queue.empty():
|
||||||
try:
|
try:
|
||||||
self._command_queue.get_nowait()
|
self._command_queue.get_nowait()
|
||||||
self._command_queue.task_done()
|
self._command_queue.task_done()
|
||||||
dropped += 1
|
|
||||||
except asyncio.QueueEmpty:
|
except asyncio.QueueEmpty:
|
||||||
break
|
break
|
||||||
_LOGGER.debug("MQTT dropped %s queued command(s)", dropped)
|
|
||||||
|
|
||||||
_LOGGER.debug("MQTT shutdown sequence complete")
|
|
||||||
|
|
||||||
async def wait_until_ready(
|
async def wait_until_ready(
|
||||||
self, device: OasisDevice, timeout: float = 10.0, request_status: bool = True
|
self, device: OasisDevice, timeout: float = 10.0, request_status: bool = True
|
||||||
@@ -617,9 +586,6 @@ class OasisMqttClient(OasisClientProtocol):
|
|||||||
return
|
return
|
||||||
|
|
||||||
while not self._command_queue.empty():
|
while not self._command_queue.empty():
|
||||||
if not self._client:
|
|
||||||
break
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
serial, payload = self._command_queue.get_nowait()
|
serial, payload = self._command_queue.get_nowait()
|
||||||
except asyncio.QueueEmpty:
|
except asyncio.QueueEmpty:
|
||||||
@@ -633,6 +599,7 @@ class OasisMqttClient(OasisClientProtocol):
|
|||||||
serial,
|
serial,
|
||||||
payload,
|
payload,
|
||||||
)
|
)
|
||||||
|
self._command_queue.task_done()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
topic = f"{serial}/COMMAND/CMD"
|
topic = f"{serial}/COMMAND/CMD"
|
||||||
@@ -642,10 +609,11 @@ class OasisMqttClient(OasisClientProtocol):
|
|||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Failed to flush queued command for %s, re-queuing", serial
|
"Failed to flush queued command for %s, re-queuing", serial
|
||||||
)
|
)
|
||||||
# Put it back; we'll try again on next reconnect
|
# Put it back and break; we'll try again on next reconnect
|
||||||
await self._enqueue_command(serial, payload)
|
await self._enqueue_command(serial, payload)
|
||||||
finally:
|
self._command_queue.task_done()
|
||||||
# Ensure we always balance the get(), even on cancellation
|
break
|
||||||
|
|
||||||
self._command_queue.task_done()
|
self._command_queue.task_done()
|
||||||
|
|
||||||
async def _publish_command(
|
async def _publish_command(
|
||||||
@@ -691,15 +659,9 @@ class OasisMqttClient(OasisClientProtocol):
|
|||||||
|
|
||||||
async def _mqtt_loop(self) -> None:
|
async def _mqtt_loop(self) -> None:
|
||||||
"""
|
"""
|
||||||
Run the MQTT WebSocket connection loop that maintains connection, subscriptions,
|
Run the MQTT WebSocket connection loop that maintains connection, subscriptions, and message handling.
|
||||||
and message handling.
|
|
||||||
|
|
||||||
This background coroutine establishes a persistent WSS MQTT connection to the
|
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.
|
||||||
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()
|
loop = asyncio.get_running_loop()
|
||||||
tls_context = await loop.run_in_executor(None, ssl.create_default_context)
|
tls_context = await loop.run_in_executor(None, ssl.create_default_context)
|
||||||
|
|||||||
@@ -12,19 +12,19 @@ try:
|
|||||||
TRACKS: Final[dict[int, dict[str, Any]]] = {
|
TRACKS: Final[dict[int, dict[str, Any]]] = {
|
||||||
int(k): v for k, v in json.load(file).items()
|
int(k): v for k, v in json.load(file).items()
|
||||||
}
|
}
|
||||||
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
except Exception: # ignore: broad-except
|
||||||
TRACKS = {}
|
TRACKS = {}
|
||||||
|
|
||||||
AUTOPLAY_MAP: Final[dict[str, str]] = {
|
AUTOPLAY_MAP: Final[dict[str, str]] = {
|
||||||
"1": "Off", # display off (disabled) first
|
"0": "on",
|
||||||
"0": "Immediately",
|
"1": "off",
|
||||||
"2": "After 5 minutes",
|
"2": "5 minutes",
|
||||||
"3": "After 10 minutes",
|
"3": "10 minutes",
|
||||||
"4": "After 30 minutes",
|
"4": "30 minutes",
|
||||||
"6": "After 1 hour",
|
"6": "1 hour",
|
||||||
"7": "After 6 hours",
|
"7": "6 hours",
|
||||||
"8": "After 12 hours",
|
"8": "12 hours",
|
||||||
"5": "After 24 hours", # purposefully placed so time is incrementally displayed
|
"5": "24 hours",
|
||||||
}
|
}
|
||||||
|
|
||||||
ERROR_CODE_MAP: Final[dict[int, str]] = {
|
ERROR_CODE_MAP: Final[dict[int, str]] = {
|
||||||
@@ -94,28 +94,17 @@ LED_EFFECTS: Final[dict[str, str]] = {
|
|||||||
"41": "Color Comets",
|
"41": "Color Comets",
|
||||||
}
|
}
|
||||||
|
|
||||||
STATUS_BOOTING: Final[int] = 0
|
STATUS_CODE_SLEEPING: Final = 6
|
||||||
STATUS_STOPPED: Final[int] = 2
|
|
||||||
STATUS_CENTERING: Final[int] = 3
|
|
||||||
STATUS_PLAYING: Final[int] = 4
|
|
||||||
STATUS_PAUSED: Final[int] = 5
|
|
||||||
STATUS_SLEEPING: Final[int] = 6
|
|
||||||
STATUS_ERROR: Final[int] = 9
|
|
||||||
STATUS_UPDATING: Final[int] = 11
|
|
||||||
STATUS_DOWNLOADING: Final[int] = 13
|
|
||||||
STATUS_BUSY: Final[int] = 14
|
|
||||||
STATUS_LIVE: Final[int] = 15
|
|
||||||
|
|
||||||
STATUS_CODE_MAP: Final[dict[int, str]] = {
|
STATUS_CODE_MAP: Final[dict[int, str]] = {
|
||||||
STATUS_BOOTING: "booting",
|
0: "booting",
|
||||||
STATUS_STOPPED: "stopped",
|
2: "stopped",
|
||||||
STATUS_CENTERING: "centering",
|
3: "centering",
|
||||||
STATUS_PLAYING: "playing",
|
4: "playing",
|
||||||
STATUS_PAUSED: "paused",
|
5: "paused",
|
||||||
STATUS_SLEEPING: "sleeping",
|
STATUS_CODE_SLEEPING: "sleeping",
|
||||||
STATUS_ERROR: "error",
|
9: "error",
|
||||||
STATUS_UPDATING: "updating",
|
11: "updating",
|
||||||
STATUS_DOWNLOADING: "downloading",
|
13: "downloading",
|
||||||
STATUS_BUSY: "busy",
|
14: "busy",
|
||||||
STATUS_LIVE: "live",
|
15: "live",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ from .const import (
|
|||||||
ERROR_CODE_MAP,
|
ERROR_CODE_MAP,
|
||||||
LED_EFFECTS,
|
LED_EFFECTS,
|
||||||
STATUS_CODE_MAP,
|
STATUS_CODE_MAP,
|
||||||
STATUS_ERROR,
|
STATUS_CODE_SLEEPING,
|
||||||
STATUS_SLEEPING,
|
|
||||||
TRACKS,
|
TRACKS,
|
||||||
)
|
)
|
||||||
from .utils import _bit_to_bool, _parse_int, create_svg, decrypt_svg_content
|
from .utils import _bit_to_bool, _parse_int, create_svg, decrypt_svg_content
|
||||||
@@ -70,6 +69,7 @@ class OasisDevice:
|
|||||||
cloud: OasisCloudClient | None = None,
|
cloud: OasisCloudClient | None = None,
|
||||||
client: OasisClientProtocol | None = None,
|
client: OasisClientProtocol | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
# Transport
|
||||||
"""
|
"""
|
||||||
Initialize an OasisDevice with identification, network, transport references, and default state fields.
|
Initialize an OasisDevice with identification, network, transport references, and default state fields.
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ class OasisDevice:
|
|||||||
Returns:
|
Returns:
|
||||||
`true` if the device is sleeping, `false` otherwise.
|
`true` if the device is sleeping, `false` otherwise.
|
||||||
"""
|
"""
|
||||||
return self.status_code == STATUS_SLEEPING
|
return self.status_code == STATUS_CODE_SLEEPING
|
||||||
|
|
||||||
def attach_client(self, client: OasisClientProtocol) -> None:
|
def attach_client(self, client: OasisClientProtocol) -> None:
|
||||||
"""Attach a transport client (MQTT, HTTP, etc.) to this device."""
|
"""Attach a transport client (MQTT, HTTP, etc.) to this device."""
|
||||||
@@ -343,7 +343,7 @@ class OasisDevice:
|
|||||||
Returns:
|
Returns:
|
||||||
str: The mapped error message when the device status indicates an error (status code 9); `None` otherwise.
|
str: The mapped error message when the device status indicates an error (status code 9); `None` otherwise.
|
||||||
"""
|
"""
|
||||||
if self.status_code == STATUS_ERROR:
|
if self.status_code == 9:
|
||||||
return ERROR_CODE_MAP.get(self.error, f"Unknown ({self.error})")
|
return ERROR_CODE_MAP.get(self.error, f"Unknown ({self.error})")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -76,15 +76,15 @@
|
|||||||
"autoplay": {
|
"autoplay": {
|
||||||
"name": "Autoplay",
|
"name": "Autoplay",
|
||||||
"state": {
|
"state": {
|
||||||
"1": "Off",
|
"0": "on",
|
||||||
"0": "Immediately",
|
"1": "off",
|
||||||
"2": "After 5 minutes",
|
"2": "5 minutes",
|
||||||
"3": "After 10 minutes",
|
"3": "10 minutes",
|
||||||
"4": "After 30 minutes",
|
"4": "30 minutes",
|
||||||
"6": "After 1 hour",
|
"6": "1 hour",
|
||||||
"7": "After 6 hours",
|
"7": "6 hours",
|
||||||
"8": "After 12 hours",
|
"8": "12 hours",
|
||||||
"5": "After 24 hours"
|
"5": "24 hours"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playlist": {
|
"playlist": {
|
||||||
|
|||||||
@@ -76,15 +76,15 @@
|
|||||||
"autoplay": {
|
"autoplay": {
|
||||||
"name": "Autoplay",
|
"name": "Autoplay",
|
||||||
"state": {
|
"state": {
|
||||||
"1": "Off",
|
"0": "on",
|
||||||
"0": "Immediately",
|
"1": "off",
|
||||||
"2": "After 5 minutes",
|
"2": "5 minutes",
|
||||||
"3": "After 10 minutes",
|
"3": "10 minutes",
|
||||||
"4": "After 30 minutes",
|
"4": "30 minutes",
|
||||||
"6": "After 1 hour",
|
"6": "1 hour",
|
||||||
"7": "After 6 hours",
|
"7": "6 hours",
|
||||||
"8": "After 12 hours",
|
"8": "12 hours",
|
||||||
"5": "After 24 hours"
|
"5": "24 hours"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playlist": {
|
"playlist": {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from . import OasisDeviceConfigEntry, setup_platform_from_coordinator
|
from . import OasisDeviceConfigEntry, setup_platform_from_coordinator
|
||||||
from .entity import OasisDeviceEntity
|
from .entity import OasisDeviceEntity
|
||||||
from .pyoasiscontrol import OasisDevice
|
from .pyoasiscontrol import OasisDevice
|
||||||
from .pyoasiscontrol.const import STATUS_UPDATING
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -72,7 +71,7 @@ class OasisDeviceUpdateEntity(OasisDeviceEntity, UpdateEntity):
|
|||||||
@property
|
@property
|
||||||
def in_progress(self) -> bool | int:
|
def in_progress(self) -> bool | int:
|
||||||
"""Update installation progress."""
|
"""Update installation progress."""
|
||||||
if self.device.status_code == STATUS_UPDATING:
|
if self.device.status_code == 11:
|
||||||
return self.device.download_progress
|
return self.device.download_progress
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user