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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user