diff --git a/custom_components/oasis_mini/button.py b/custom_components/oasis_mini/button.py index ad295f3..0854e4c 100644 --- a/custom_components/oasis_mini/button.py +++ b/custom_components/oasis_mini/button.py @@ -78,4 +78,3 @@ class OasisDeviceButtonEntity(OasisDeviceEntity, ButtonEntity): async def async_press(self) -> None: """Press the button.""" await self.entity_description.press_fn(self.device) - await self.coordinator.async_request_refresh() diff --git a/custom_components/oasis_mini/coordinator.py b/custom_components/oasis_mini/coordinator.py index da04913..355a0f3 100644 --- a/custom_components/oasis_mini/coordinator.py +++ b/custom_components/oasis_mini/coordinator.py @@ -33,7 +33,7 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]): hass, _LOGGER, name=DOMAIN, - update_interval=timedelta(seconds=10), + update_interval=timedelta(minutes=10), always_update=False, ) self.cloud_client = cloud_client diff --git a/custom_components/oasis_mini/light.py b/custom_components/oasis_mini/light.py index f423cf4..1f131d7 100644 --- a/custom_components/oasis_mini/light.py +++ b/custom_components/oasis_mini/light.py @@ -53,7 +53,7 @@ class OasisDeviceLightEntity(OasisDeviceEntity, LightEntity): @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - scale = (1, self.device.max_brightness) + scale = (1, self.device.brightness_max) return value_to_brightness(scale, self.device.brightness) @property @@ -99,15 +99,14 @@ class OasisDeviceLightEntity(OasisDeviceEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await self.device.async_set_led(brightness=0) - await self.coordinator.async_request_refresh() async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" if brightness := kwargs.get(ATTR_BRIGHTNESS): - scale = (1, self.device.max_brightness) + scale = (1, self.device.brightness_max) brightness = math.ceil(brightness_to_value(scale, brightness)) else: - brightness = self.device.brightness or 100 + brightness = self.device.brightness or self.device.brightness_on if color := kwargs.get(ATTR_RGB_COLOR): color = f"#{color_rgb_to_hex(*color)}" @@ -120,4 +119,3 @@ class OasisDeviceLightEntity(OasisDeviceEntity, LightEntity): await self.device.async_set_led( brightness=brightness, color=color, led_effect=led_effect ) - await self.coordinator.async_request_refresh() diff --git a/custom_components/oasis_mini/media_player.py b/custom_components/oasis_mini/media_player.py index 29803d9..5ad6cf1 100644 --- a/custom_components/oasis_mini/media_player.py +++ b/custom_components/oasis_mini/media_player.py @@ -134,19 +134,16 @@ class OasisDeviceMediaPlayerEntity(OasisDeviceEntity, MediaPlayerEntity): """Send pause command.""" self.abort_if_busy() await self.device.async_pause() - await self.coordinator.async_request_refresh() async def async_media_play(self) -> None: """Send play command.""" self.abort_if_busy() await self.device.async_play() - await self.coordinator.async_request_refresh() async def async_media_stop(self) -> None: """Send stop command.""" self.abort_if_busy() await self.device.async_stop() - await self.coordinator.async_request_refresh() async def async_set_repeat(self, repeat: RepeatMode) -> None: """Set repeat mode.""" @@ -154,7 +151,6 @@ class OasisDeviceMediaPlayerEntity(OasisDeviceEntity, MediaPlayerEntity): repeat != RepeatMode.OFF and not (repeat == RepeatMode.ONE and self.repeat == RepeatMode.ALL) ) - await self.coordinator.async_request_refresh() async def async_media_previous_track(self) -> None: """Send previous track command.""" @@ -162,7 +158,6 @@ class OasisDeviceMediaPlayerEntity(OasisDeviceEntity, MediaPlayerEntity): if (index := self.device.playlist_index - 1) < 0: index = len(self.device.playlist) - 1 await self.device.async_change_track(index) - await self.coordinator.async_request_refresh() async def async_media_next_track(self) -> None: """Send next track command.""" @@ -170,7 +165,6 @@ class OasisDeviceMediaPlayerEntity(OasisDeviceEntity, MediaPlayerEntity): if (index := self.device.playlist_index + 1) >= len(self.device.playlist): index = 0 await self.device.async_change_track(index) - await self.coordinator.async_request_refresh() async def async_play_media( self, @@ -220,10 +214,7 @@ class OasisDeviceMediaPlayerEntity(OasisDeviceEntity, MediaPlayerEntity): ): await device.async_play() - await self.coordinator.async_request_refresh() - async def async_clear_playlist(self) -> None: """Clear players playlist.""" self.abort_if_busy() await self.device.async_clear_playlist() - await self.coordinator.async_request_refresh() diff --git a/custom_components/oasis_mini/number.py b/custom_components/oasis_mini/number.py index 08af451..a59c2b8 100644 --- a/custom_components/oasis_mini/number.py +++ b/custom_components/oasis_mini/number.py @@ -63,8 +63,8 @@ class OasisDeviceNumberEntity(OasisDeviceEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set new value.""" + value = int(value) if self.entity_description.key == "ball_speed": await self.device.async_set_ball_speed(value) elif self.entity_description.key == "led_speed": await self.device.async_set_led(led_speed=value) - await self.coordinator.async_request_refresh() diff --git a/custom_components/oasis_mini/pyoasiscontrol/clients/http_client.py b/custom_components/oasis_mini/pyoasiscontrol/clients/http_client.py index 513327a..db6159f 100644 --- a/custom_components/oasis_mini/pyoasiscontrol/clients/http_client.py +++ b/custom_components/oasis_mini/pyoasiscontrol/clients/http_client.py @@ -7,9 +7,7 @@ from typing import Any from aiohttp import ClientSession -from ..const import AUTOPLAY_MAP from ..device import OasisDevice -from ..utils import _bit_to_bool, _parse_int from .transport import OasisClientProtocol _LOGGER = logging.getLogger(__name__) @@ -179,37 +177,4 @@ class OasisHttpClient(OasisClientProtocol): return _LOGGER.debug("Status for %s: %s", device.serial_number, raw_status) - - values = raw_status.split(";") - if len(values) < 7: - _LOGGER.warning( - "Unexpected status format for %s: %s", device.serial_number, values - ) - return - - playlist = [_parse_int(track) for track in values[3].split(",") if track] - shift = len(values) - 18 if len(values) > 17 else 0 - - try: - status: dict[str, Any] = { - "status_code": _parse_int(values[0]), - "error": _parse_int(values[1]), - "ball_speed": _parse_int(values[2]), - "playlist": playlist, - "playlist_index": min(_parse_int(values[4]), len(playlist)), - "progress": _parse_int(values[5]), - "led_effect": values[6], - "led_speed": _parse_int(values[8]), - "brightness": _parse_int(values[9]), - "color": values[10] if "#" in values[10] else None, - "busy": _bit_to_bool(values[11 + shift]), - "download_progress": _parse_int(values[12 + shift]), - "max_brightness": _parse_int(values[13 + shift]), - "repeat_playlist": _bit_to_bool(values[15 + shift]), - "autoplay": AUTOPLAY_MAP.get(value := values[16 + shift], value), - } - except Exception: # noqa: BLE001 - _LOGGER.exception("Error parsing HTTP status for %s", device.serial_number) - return - - device.update_from_status_dict(status) + device.update_from_status_string(raw_status) diff --git a/custom_components/oasis_mini/pyoasiscontrol/clients/mqtt_client.py b/custom_components/oasis_mini/pyoasiscontrol/clients/mqtt_client.py index 44af614..df39c35 100644 --- a/custom_components/oasis_mini/pyoasiscontrol/clients/mqtt_client.py +++ b/custom_components/oasis_mini/pyoasiscontrol/clients/mqtt_client.py @@ -11,9 +11,8 @@ from typing import Any, Final import aiomqtt -from ..const import AUTOPLAY_MAP from ..device import OasisDevice -from ..utils import _bit_to_bool +from ..utils import _bit_to_bool, _parse_int from .transport import OasisClientProtocol _LOGGER = logging.getLogger(__name__) @@ -26,6 +25,9 @@ USERNAME: Final = "YXBw" PASSWORD: Final = "RWdETFlKMDczfi4t" RECONNECT_INTERVAL: Final = 4 +# Command queue behaviour +MAX_PENDING_COMMANDS: Final = 10 + class OasisMqttClient(OasisClientProtocol): """MQTT-based Oasis transport using WSS. @@ -58,6 +60,11 @@ class OasisMqttClient(OasisClientProtocol): self._subscribed_serials: set[str] = set() self._subscription_lock = asyncio.Lock() + # Pending command queue: (serial, payload) + self._command_queue: asyncio.Queue[tuple[str, str]] = asyncio.Queue( + maxsize=MAX_PENDING_COMMANDS + ) + def register_device(self, device: OasisDevice) -> None: """Register a device so MQTT messages can be routed to it.""" if not device.serial_number: @@ -70,6 +77,10 @@ class OasisMqttClient(OasisClientProtocol): self._first_status_events.setdefault(serial, asyncio.Event()) self._mac_events.setdefault(serial, asyncio.Event()) + # Attach ourselves as the client if the device doesn't already have one + if not device.client: + device.attach_client(self) + # If we're already connected, subscribe to this device's topics if self._client is not None: try: @@ -81,9 +92,6 @@ class OasisMqttClient(OasisClientProtocol): "Could not schedule subscription for %s (no running loop)", serial ) - if not device.client: - device.attach_client(self) - def unregister_device(self, device: OasisDevice) -> None: serial = device.serial_number if not serial: @@ -168,6 +176,14 @@ class OasisMqttClient(OasisClientProtocol): finally: self._client = None + # Drop pending commands on stop + while not self._command_queue.empty(): + try: + self._command_queue.get_nowait() + self._command_queue.task_done() + except asyncio.QueueEmpty: + break + async def wait_until_ready( self, device: OasisDevice, timeout: float = 10.0, request_status: bool = True ) -> bool: @@ -260,7 +276,7 @@ class OasisMqttClient(OasisClientProtocol): brightness: int, ) -> None: payload = f"WRILED={led_effect};0;{color};{led_speed};{brightness}" - await self._publish_command(device, payload) + await self._publish_command(device, payload, bool(brightness)) async def async_send_sleep_command(self, device: OasisDevice) -> None: await self._publish_command(device, "CMDSLEEP") @@ -328,7 +344,7 @@ class OasisMqttClient(OasisClientProtocol): await self._publish_command(device, payload) async def async_send_play_command(self, device: OasisDevice) -> None: - await self._publish_command(device, "CMDPLAY") + await self._publish_command(device, "CMDPLAY", True) async def async_send_pause_command(self, device: OasisDevice) -> None: await self._publish_command(device, "CMDPAUSE") @@ -339,21 +355,96 @@ class OasisMqttClient(OasisClientProtocol): async def async_send_reboot_command(self, device: OasisDevice) -> None: await self._publish_command(device, "CMDBOOT") + async def async_get_all(self, device: OasisDevice) -> None: + """Request FULLSTATUS + SCHEDULE (compact snapshot).""" + await self._publish_command(device, "GETALL") + async def async_get_status(self, device: OasisDevice) -> None: """Ask device to publish STATUS topics.""" await self._publish_command(device, "GETSTATUS") - async def _publish_command(self, device: OasisDevice, payload: str) -> None: - if not self._client: - raise RuntimeError("MQTT client not connected yet") + async def _enqueue_command(self, serial: str, payload: str) -> None: + """Queue a command to be sent when connected. + If the queue is full, drop the oldest command to make room. + """ + if self._command_queue.full(): + try: + dropped = self._command_queue.get_nowait() + self._command_queue.task_done() + _LOGGER.debug( + "Command queue full, dropping oldest command: %s", dropped + ) + except asyncio.QueueEmpty: + # race: became empty between full() and get_nowait() + pass + + await self._command_queue.put((serial, payload)) + _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.""" + if not self._client: + return + + while not self._command_queue.empty(): + try: + serial, payload = self._command_queue.get_nowait() + except asyncio.QueueEmpty: + break + + try: + # Skip commands for unknown devices + if serial not in self._devices: + _LOGGER.debug( + "Skipping queued command for unknown device %s: %s", + serial, + payload, + ) + self._command_queue.task_done() + continue + + topic = f"{serial}/COMMAND/CMD" + _LOGGER.debug("Flushing queued MQTT command %s => %s", topic, payload) + await self._client.publish(topic, payload.encode(), qos=1) + except Exception: + _LOGGER.debug( + "Failed to flush queued command for %s, re-queuing", serial + ) + # Put it back and break; we'll try again on next reconnect + await self._enqueue_command(serial, payload) + self._command_queue.task_done() + break + + self._command_queue.task_done() + + async def _publish_command( + self, device: OasisDevice, payload: str, wake: bool = False + ) -> None: serial = device.serial_number if not serial: - raise RuntimeError("Device has no serial_number set") + raise RuntimeError("Device has no serial number set") + + if wake and device.is_sleeping: + await self.async_get_all(device) + + # If not connected, just queue the command + if not self._client or not self._connected_event.is_set(): + _LOGGER.debug( + "MQTT not connected, queueing command for %s: %s", serial, payload + ) + await self._enqueue_command(serial, payload) + return topic = f"{serial}/COMMAND/CMD" - _LOGGER.debug("MQTT publish %s => %s", topic, payload) - await self._client.publish(topic, payload.encode(), qos=1) + try: + _LOGGER.debug("MQTT publish %s => %s", topic, payload) + await self._client.publish(topic, payload.encode(), qos=1) + except Exception: + _LOGGER.debug( + "MQTT publish failed, queueing command for %s: %s", serial, payload + ) + await self._enqueue_command(serial, payload) async def _mqtt_loop(self) -> None: loop = asyncio.get_running_loop() @@ -361,12 +452,7 @@ class OasisMqttClient(OasisClientProtocol): while not self._stop_event.is_set(): try: - _LOGGER.debug( - "Connecting MQTT WSS to wss://%s:%s/%s", - HOST, - PORT, - PATH, - ) + _LOGGER.info("Connecting MQTT WSS to wss://%s:%s/%s", HOST, PORT, PATH) async with aiomqtt.Client( hostname=HOST, @@ -386,6 +472,9 @@ class OasisMqttClient(OasisClientProtocol): # Subscribe only to STATUS topics for known devices await self._resubscribe_all() + # Flush any queued commands now that we're connected + await self._flush_pending_commands() + async for msg in client.messages: if self._stop_event.is_set(): break @@ -394,13 +483,13 @@ class OasisMqttClient(OasisClientProtocol): except asyncio.CancelledError: break except Exception: - _LOGGER.debug("MQTT connection error") + _LOGGER.info("MQTT connection error") finally: if self._connected_event.is_set(): self._connected_event.clear() if self._connected_at: - _LOGGER.debug( + _LOGGER.info( "MQTT was connected for %s", datetime.now(UTC) - self._connected_at, ) @@ -409,14 +498,13 @@ class OasisMqttClient(OasisClientProtocol): self._subscribed_serials.clear() if not self._stop_event.is_set(): - _LOGGER.debug( + _LOGGER.info( "Disconnected from broker, retrying in %.1fs", RECONNECT_INTERVAL ) 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.""" - topic_str = str(msg.topic) if msg.topic is not None else "" payload = msg.payload.decode(errors="replace") @@ -429,7 +517,6 @@ class OasisMqttClient(OasisClientProtocol): device = self._devices.get(serial) if not device: - # Ignore devices we don't know about _LOGGER.debug("Received MQTT for unknown device %s: %s", serial, topic_str) return @@ -451,13 +538,13 @@ class OasisMqttClient(OasisClientProtocol): elif status_name == "LED_EFFECT": data["led_effect"] = payload elif status_name == "LED_EFFECT_COLOR": - data["led_effect_color"] = payload + data["led_color_id"] = payload elif status_name == "LED_SPEED": data["led_speed"] = int(payload) elif status_name == "LED_BRIGHTNESS": data["brightness"] = int(payload) elif status_name == "LED_MAX": - data["max_brightness"] = int(payload) + data["brightness_max"] = int(payload) elif status_name == "LED_EFFECT_PARAM": data["color"] = payload if payload.startswith("#") else None elif status_name == "SYSTEM_BUSY": @@ -467,7 +554,7 @@ class OasisMqttClient(OasisClientProtocol): elif status_name == "REPEAT_JOB": data["repeat_playlist"] = payload in ("1", "true", "True") elif status_name == "WAIT_AFTER_JOB": - data["autoplay"] = AUTOPLAY_MAP.get(payload, payload) + data["autoplay"] = _parse_int(payload) elif status_name == "AUTO_CLEAN": data["auto_clean"] = payload in ("1", "true", "True") elif status_name == "SOFTWARE_VER": @@ -494,6 +581,9 @@ class OasisMqttClient(OasisClientProtocol): data["schedule"] = payload elif status_name == "ENVIRONMENT": data["environment"] = payload + elif status_name == "FULLSTATUS": + if parsed := device.parse_status_string(payload): + data = parsed else: _LOGGER.warning( "Unknown status received for %s: %s=%s", diff --git a/custom_components/oasis_mini/pyoasiscontrol/clients/transport.py b/custom_components/oasis_mini/pyoasiscontrol/clients/transport.py index 3b3e1aa..adc0d1d 100644 --- a/custom_components/oasis_mini/pyoasiscontrol/clients/transport.py +++ b/custom_components/oasis_mini/pyoasiscontrol/clients/transport.py @@ -83,3 +83,7 @@ class OasisClientProtocol(Protocol): async def async_send_stop_command(self, device: OasisDevice) -> None: ... async def async_send_reboot_command(self, device: OasisDevice) -> None: ... + + async def async_get_all(self, device: OasisDevice) -> None: ... + + async def async_get_status(self, device: OasisDevice) -> None: ... diff --git a/custom_components/oasis_mini/pyoasiscontrol/const.py b/custom_components/oasis_mini/pyoasiscontrol/const.py index 2977c2d..591bdb6 100644 --- a/custom_components/oasis_mini/pyoasiscontrol/const.py +++ b/custom_components/oasis_mini/pyoasiscontrol/const.py @@ -21,6 +21,9 @@ AUTOPLAY_MAP: Final[dict[str, str]] = { "2": "5 minutes", "3": "10 minutes", "4": "30 minutes", + "6": "1 hour", + "7": "6 hours", + "8": "12 hours", "5": "24 hours", } @@ -91,13 +94,14 @@ LED_EFFECTS: Final[dict[str, str]] = { "41": "Color Comets", } +STATUS_CODE_SLEEPING: Final = 6 STATUS_CODE_MAP: Final[dict[int, str]] = { 0: "booting", 2: "stopped", 3: "centering", 4: "playing", 5: "paused", - 6: "sleeping", + STATUS_CODE_SLEEPING: "sleeping", 9: "error", 11: "updating", 13: "downloading", diff --git a/custom_components/oasis_mini/pyoasiscontrol/device.py b/custom_components/oasis_mini/pyoasiscontrol/device.py index 8a25d47..40449e0 100644 --- a/custom_components/oasis_mini/pyoasiscontrol/device.py +++ b/custom_components/oasis_mini/pyoasiscontrol/device.py @@ -5,7 +5,14 @@ from __future__ import annotations import logging from typing import TYPE_CHECKING, Any, Callable, Final, Iterable -from .const import ERROR_CODE_MAP, LED_EFFECTS, STATUS_CODE_MAP, TRACKS +from .const import ( + ERROR_CODE_MAP, + LED_EFFECTS, + STATUS_CODE_MAP, + STATUS_CODE_SLEEPING, + TRACKS, +) +from .utils import _bit_to_bool, _parse_int if TYPE_CHECKING: # avoid runtime circular imports from .clients.transport import OasisClientProtocol @@ -18,6 +25,7 @@ LED_SPEED_MAX: Final = 90 LED_SPEED_MIN: Final = -90 _STATE_FIELDS = ( + "auto_clean", "autoplay", "ball_speed", "brightness", @@ -28,7 +36,6 @@ _STATE_FIELDS = ( "led_effect", "led_speed", "mac_address", - "max_brightness", "playlist", "playlist_index", "progress", @@ -69,17 +76,19 @@ class OasisDevice: # Status self.auto_clean: bool = False - self.autoplay: str = "off" + self.autoplay: int = 0 self.ball_speed: int = BALL_SPEED_MIN - self.brightness: int = 0 + self._brightness: int = 0 + self.brightness_max: int = 200 + self.brightness_on: int = 0 self.busy: bool = False self.color: str | None = None self.download_progress: int = 0 self.error: int = 0 + self.led_color_id: str = "0" self.led_effect: str = "0" self.led_speed: int = 0 self.mac_address: str | None = None - self.max_brightness: int = 200 self.playlist: list[int] = [] self.playlist_index: int = 0 self.progress: int = 0 @@ -99,6 +108,22 @@ class OasisDevice: # Track metadata cache (used if you hydrate from cloud) self._track: dict | None = None + @property + def brightness(self) -> int: + """Return the brightness.""" + return 0 if self.is_sleeping else self._brightness + + @brightness.setter + def brightness(self, value: int) -> None: + self._brightness = value + if value: + self.brightness_on = value + + @property + def is_sleeping(self) -> bool: + """Return `True` if the status is set to sleeping.""" + return self.status_code == STATUS_CODE_SLEEPING + def attach_client(self, client: OasisClientProtocol) -> None: """Attach a transport client (MQTT, HTTP, etc.) to this device.""" self._client = client @@ -142,6 +167,66 @@ class OasisDevice: if changed: 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) + """ + if not raw_status: + return None + + values = raw_status.split(";") + + # We rely on indices 0..17 existing (18 fields) + if (n := len(values)) < 18: + _LOGGER.warning( + "Unexpected status format for %s: %s", self.serial_number, values + ) + return None + + playlist = [_parse_int(track) for track in values[3].split(",") if track] + + try: + status: dict[str, Any] = { + "status_code": _parse_int(values[0]), + "error": _parse_int(values[1]), + "ball_speed": _parse_int(values[2]), + "playlist": playlist, + "playlist_index": min(_parse_int(values[4]), len(playlist)), + "progress": _parse_int(values[5]), + "led_effect": values[6], + "led_color_id": values[7], + "led_speed": _parse_int(values[8]), + "brightness": _parse_int(values[9]), + "color": values[10] if "#" in values[10] else None, + "busy": _bit_to_bool(values[11]), + "download_progress": _parse_int(values[12]), + "brightness_max": _parse_int(values[13]), + "wifi_connected": _bit_to_bool(values[14]), + "repeat_playlist": _bit_to_bool(values[15]), + "autoplay": _parse_int(values[16]), + "auto_clean": _bit_to_bool(values[17]), + } + + # Optional trailing field(s) + if n > 18: + status["software_version"] = values[18] + + except Exception: # noqa: BLE001 + _LOGGER.exception( + "Error parsing status string for %s: %r", self.serial_number, raw_status + ) + return None + + return status + + def update_from_status_string(self, raw_status: str) -> None: + """Parse and apply a raw status string.""" + 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 {field: getattr(self, field) for field in _STATE_FIELDS} @@ -259,7 +344,7 @@ class OasisDevice: raise ValueError("Invalid led effect specified") if not LED_SPEED_MIN <= led_speed <= LED_SPEED_MAX: raise ValueError("Invalid led speed specified") - if not 0 <= brightness <= self.max_brightness: + if not 0 <= brightness <= self.brightness_max: raise ValueError("Invalid brightness specified") client = self._require_client() diff --git a/custom_components/oasis_mini/select.py b/custom_components/oasis_mini/select.py index 8e2da4a..b741314 100644 --- a/custom_components/oasis_mini/select.py +++ b/custom_components/oasis_mini/select.py @@ -17,6 +17,8 @@ from .entity import OasisDeviceEntity from .pyoasiscontrol import OasisDevice from .pyoasiscontrol.const import AUTOPLAY_MAP, TRACKS +AUTOPLAY_MAP_LIST = list(AUTOPLAY_MAP) + def playlists_update_handler(entity: OasisDeviceSelectEntity) -> None: """Handle playlists updates.""" @@ -96,15 +98,17 @@ DESCRIPTORS = ( OasisDeviceSelectEntityDescription( key="autoplay", translation_key="autoplay", - options=list(AUTOPLAY_MAP.values()), - current_value=lambda device: device.autoplay, - select_fn=lambda device, option: device.async_set_autoplay(option), + options=AUTOPLAY_MAP_LIST, + current_value=lambda device: str(device.autoplay), + select_fn=lambda device, index: ( + device.async_set_autoplay(AUTOPLAY_MAP_LIST[index]) + ), ), OasisDeviceSelectEntityDescription( key="queue", translation_key="queue", current_value=lambda device: (device.playlist.copy(), device.playlist_index), - select_fn=lambda device, option: device.async_change_track(option), + select_fn=lambda device, index: device.async_change_track(index), update_handler=queue_update_handler, ), ) @@ -113,8 +117,8 @@ CLOUD_DESCRIPTORS = ( key="playlists", translation_key="playlist", current_value=lambda device: (device.playlists, device.playlist.copy()), - select_fn=lambda device, option: device.async_set_playlist( - [pattern["id"] for pattern in device.playlists[option]["patterns"]] + select_fn=lambda device, index: device.async_set_playlist( + [pattern["id"] for pattern in device.playlists[index]["patterns"]] ), update_handler=playlists_update_handler, ), @@ -140,7 +144,6 @@ class OasisDeviceSelectEntity(OasisDeviceEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Change the selected option.""" await self.entity_description.select_fn(self.device, self.options.index(option)) - await self.coordinator.async_request_refresh() @callback def _handle_coordinator_update(self) -> None: @@ -152,8 +155,8 @@ class OasisDeviceSelectEntity(OasisDeviceEntity, SelectEntity): if update_handler := self.entity_description.update_handler: update_handler(self) else: - self._attr_current_option = getattr( - self.device, self.entity_description.key + self._attr_current_option = str( + getattr(self.device, self.entity_description.key) ) if self.hass: return super()._handle_coordinator_update() diff --git a/custom_components/oasis_mini/sensor.py b/custom_components/oasis_mini/sensor.py index 3e03378..3048950 100644 --- a/custom_components/oasis_mini/sensor.py +++ b/custom_components/oasis_mini/sensor.py @@ -52,8 +52,7 @@ DESCRIPTORS = { entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ) - for key in ("error", "status") - # for key in ("error", "led_color_id", "status") + for key in ("error", "led_color_id", "status") # for key in ("error_message", "led_color_id", "status") } diff --git a/custom_components/oasis_mini/strings.json b/custom_components/oasis_mini/strings.json index 1f029c4..b279980 100755 --- a/custom_components/oasis_mini/strings.json +++ b/custom_components/oasis_mini/strings.json @@ -74,7 +74,18 @@ }, "select": { "autoplay": { - "name": "Autoplay" + "name": "Autoplay", + "state": { + "0": "on", + "1": "off", + "2": "5 minutes", + "3": "10 minutes", + "4": "30 minutes", + "6": "1 hour", + "7": "6 hours", + "8": "12 hours", + "5": "24 hours" + } }, "playlist": { "name": "Playlist" diff --git a/custom_components/oasis_mini/translations/en.json b/custom_components/oasis_mini/translations/en.json index eef05e3..7639f55 100755 --- a/custom_components/oasis_mini/translations/en.json +++ b/custom_components/oasis_mini/translations/en.json @@ -74,7 +74,18 @@ }, "select": { "autoplay": { - "name": "Autoplay" + "name": "Autoplay", + "state": { + "0": "on", + "1": "off", + "2": "5 minutes", + "3": "10 minutes", + "4": "30 minutes", + "6": "1 hour", + "7": "6 hours", + "8": "12 hours", + "5": "24 hours" + } }, "playlist": { "name": "Playlist"