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

Don't wait on devices to initialize during coordinator update, implement dispatcher for device initialization/setup

This commit is contained in:
Nathan Spencer
2025-11-25 16:29:36 +00:00
parent d9fa3b8c9e
commit eecf5e90dc
4 changed files with 89 additions and 39 deletions

View File

@@ -10,9 +10,9 @@ from homeassistant.const import CONF_EMAIL, Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.helpers.entity_registry as er import homeassistant.helpers.entity_registry as er
import homeassistant.util.dt as dt_util
from .const import DOMAIN from .const import DOMAIN
from .coordinator import OasisDeviceCoordinator from .coordinator import OasisDeviceCoordinator
@@ -56,19 +56,12 @@ def setup_platform_from_coordinator(
update_before_add: If true, entities will be updated before being added. update_before_add: If true, entities will be updated before being added.
""" """
coordinator = entry.runtime_data coordinator = entry.runtime_data
known_serials: set[str] = set() known_serials: set[str] = set()
signal = coordinator._device_initialized_signal
@callback @callback
def _check_devices() -> None: def _check_devices() -> None:
""" """Add entities for any initialized devices not yet seen."""
Detect newly discovered Oasis devices from the coordinator and register their entities.
Scans the coordinator's current device list for devices with a serial number that has not
been seen before. For any newly discovered devices, creates entity instances via
make_entities and adds them to Home Assistant using async_add_entities with the
update_before_add flag. Does not return a value.
"""
devices = coordinator.data or [] devices = coordinator.data or []
new_devices: list[OasisDevice] = [] new_devices: list[OasisDevice] = []
@@ -86,11 +79,33 @@ def setup_platform_from_coordinator(
if entities := make_entities(new_devices): if entities := make_entities(new_devices):
async_add_entities(entities, update_before_add) async_add_entities(entities, update_before_add)
# Initial population @callback
def _handle_device_initialized(device: OasisDevice) -> None:
"""
Dispatcher callback for when a single device becomes initialized.
Adds entities immediately for that device if we haven't seen it yet.
"""
serial = device.serial_number
if not serial or serial in known_serials or not device.is_initialized:
return
known_serials.add(serial)
if entities := make_entities([device]):
async_add_entities(entities, update_before_add)
# Initial population from current coordinator data
_check_devices() _check_devices()
# Future updates (new devices discovered)
# Future changes: new devices / account re-sync via coordinator
entry.async_on_unload(coordinator.async_add_listener(_check_devices)) entry.async_on_unload(coordinator.async_add_listener(_check_devices))
# Device-level initialization events via dispatcher
entry.async_on_unload(
async_dispatcher_connect(coordinator.hass, signal, _handle_device_initialized)
)
async def async_setup_entry(hass: HomeAssistant, entry: OasisDeviceConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: OasisDeviceConfigEntry) -> bool:
""" """
@@ -127,18 +142,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: OasisDeviceConfigEntry)
entry.runtime_data = coordinator entry.runtime_data = coordinator
def _on_oasis_update() -> None:
"""
Update the coordinator's last-updated timestamp and notify its listeners.
Sets the coordinator's last_updated to the current time and triggers its update listeners so dependent entities and tasks refresh.
"""
coordinator.last_updated = dt_util.now()
coordinator.async_update_listeners()
for device in coordinator.data or []:
device.add_update_listener(_on_oasis_update)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True

View File

@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING
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.dispatcher import async_dispatcher_send
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@@ -51,15 +52,56 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]):
self.cloud_client = cloud_client self.cloud_client = cloud_client
self.mqtt_client = OasisMqttClient() self.mqtt_client = OasisMqttClient()
# Track which devices are currently considered initialized
self._initialized_serials: set[str] = set()
@property
def _device_initialized_signal(self) -> str:
"""Dispatcher signal name for device initialization events."""
return f"{DOMAIN}_{self.config_entry.entry_id}_device_initialized"
def _attach_device_listeners(self, device: OasisDevice) -> None:
"""Attach a listener so we can fire dispatcher events when a device initializes."""
def _on_device_update() -> None:
serial = device.serial_number
if not serial:
return
initialized = device.is_initialized
was_initialized = serial in self._initialized_serials
if initialized and not was_initialized:
self._initialized_serials.add(serial)
_LOGGER.debug("%s ready for setup; dispatching signal", device.name)
async_dispatcher_send(
self.hass, self._device_initialized_signal, device
)
elif not initialized and was_initialized:
self._initialized_serials.remove(serial)
_LOGGER.debug("Oasis device %s no longer initialized", serial)
self.last_updated = dt_util.now()
self.async_update_listeners()
device.add_update_listener(_on_device_update)
# Seed the initialized set if the device is already initialized
if device.is_initialized and device.serial_number:
self._initialized_serials.add(device.serial_number)
async def _async_update_data(self) -> list[OasisDevice]: async def _async_update_data(self) -> list[OasisDevice]:
""" """
Fetch and assemble the current list of OasisDevice objects, reconcile removed devices in Home Assistant, register discovered devices with MQTT, and verify per-device readiness. Fetch and assemble the current list of OasisDevice objects, reconcile removed
devices in Home Assistant, register discovered devices with MQTT, and
best-effort trigger status updates for uninitialized devices.
Returns: Returns:
A list of OasisDevice instances representing devices currently available for the account. A list of OasisDevice instances representing devices currently available for the account.
Raises: Raises:
UpdateFailed: If no devices can be read after repeated attempts or an unexpected error persists past retry limits. UpdateFailed: If an unexpected error persists past retry limits.
""" """
devices: list[OasisDevice] = [] devices: list[OasisDevice] = []
self.attempt += 1 self.attempt += 1
@@ -86,9 +128,11 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]):
name=raw.get("name"), name=raw.get("name"),
cloud=self.cloud_client, cloud=self.cloud_client,
) )
self._attach_device_listeners(device)
devices.append(device) devices.append(device)
# Handle devices removed from the account
new_serials = {d.serial_number for d in devices if d.serial_number} new_serials = {d.serial_number for d in devices if d.serial_number}
removed_serials = set(existing_by_serial) - new_serials removed_serials = set(existing_by_serial) - new_serials
@@ -119,6 +163,7 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]):
self.last_updated = dt_util.now() self.last_updated = dt_util.now()
return [] return []
# Ensure MQTT is running and devices are registered
if not self.mqtt_client.is_running: if not self.mqtt_client.is_running:
self.mqtt_client.start() self.mqtt_client.start()
self.mqtt_client.register_devices(devices) self.mqtt_client.register_devices(devices)
@@ -129,32 +174,28 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]):
except Exception: except Exception:
_LOGGER.exception("Error fetching playlists from cloud") _LOGGER.exception("Error fetching playlists from cloud")
# Best-effort: request status for devices that are not yet initialized
for device in devices: for device in devices:
try: try:
ready = await self.mqtt_client.wait_until_ready( if not device.is_initialized:
device, request_status=True await device.async_get_status()
)
if not ready:
_LOGGER.debug(
"Oasis device %s not ready yet; will retry on next update",
device.serial_number,
)
continue
device.schedule_track_refresh() device.schedule_track_refresh()
except Exception: except Exception:
_LOGGER.exception( _LOGGER.exception(
"Error preparing Oasis device %s", device.serial_number "Error requesting status for Oasis device %s; "
"will retry on future updates",
device.serial_number,
) )
self.attempt = 0 self.attempt = 0
except Exception as ex: except Exception as ex:
if self.attempt > 2 or not (devices or self.data): if self.attempt > 2 or not (devices or self.data):
raise UpdateFailed( raise UpdateFailed(
"Unexpected error talking to Oasis devices " "Unexpected error talking to Oasis devices "
f"after {self.attempt} attempts" f"after {self.attempt} attempts"
) from ex ) from ex
_LOGGER.warning( _LOGGER.warning(
"Error updating Oasis devices; reusing previous data", exc_info=ex "Error updating Oasis devices; reusing previous data", exc_info=ex
) )

View File

@@ -215,6 +215,7 @@ class OasisMqttClient(OasisClientProtocol):
self._subscribed_serials.clear() self._subscribed_serials.clear()
for serial, device in self._devices.items(): for serial, device in self._devices.items():
await self._subscribe_serial(serial) await self._subscribe_serial(serial)
if not device.is_sleeping:
await self.async_get_all(device) await self.async_get_all(device)
def start(self) -> None: def start(self) -> None:

View File

@@ -495,6 +495,11 @@ class OasisDevice:
except Exception: except Exception:
_LOGGER.exception("Error in update listener") _LOGGER.exception("Error in update listener")
async def async_get_status(self) -> None:
"""Request the device update it's current status."""
client = self._require_client()
await client.async_get_status(self)
async def async_get_mac_address(self) -> str | None: async def async_get_mac_address(self) -> str | None:
""" """
Get the device MAC address, requesting it from the attached transport client if not already known. Get the device MAC address, requesting it from the attached transport client if not already known.