"""Define a base coordinator for Bluesound entities.""" from __future__ import annotations import asyncio from collections.abc import Callable, Coroutine import contextlib from dataclasses import dataclass, replace from datetime import timedelta import logging from pyblu import Input, Player, Preset, Status, SyncStatus from pyblu.errors import PlayerUnreachableError from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator _LOGGER = logging.getLogger(__name__) NODE_OFFLINE_CHECK_TIMEOUT = timedelta(minutes=3) PRESET_AND_INPUTS_INTERVAL = timedelta(minutes=15) @dataclass class BluesoundRuntimeData: """Bluesound data class.""" player: Player sync_status: SyncStatus coordinator: BluesoundCoordinator @dataclass class BluesoundData: """Define a class to hold Bluesound data.""" sync_status: SyncStatus status: Status presets: list[Preset] inputs: list[Input] type BluesoundConfigEntry = ConfigEntry[BluesoundRuntimeData] def cancel_task(task: asyncio.Task) -> Callable[[], Coroutine[None, None, None]]: """Cancel a task.""" async def _cancel_task() -> None: task.cancel() with contextlib.suppress(asyncio.CancelledError): await task return _cancel_task class BluesoundCoordinator(DataUpdateCoordinator[BluesoundData]): """Define an object to hold Bluesound data.""" config_entry: BluesoundConfigEntry def __init__( self, hass: HomeAssistant, config_entry: BluesoundConfigEntry, player: Player, sync_status: SyncStatus, ) -> None: """Initialize.""" self.player = player self._inital_sync_status = sync_status super().__init__( hass, logger=_LOGGER, config_entry=config_entry, name=sync_status.name, ) async def _async_setup(self) -> None: preset = await self.player.presets() inputs = await self.player.inputs() status = await self.player.status() self.async_set_updated_data( BluesoundData( sync_status=self._inital_sync_status, status=status, presets=preset, inputs=inputs, ) ) status_loop_task = self.hass.async_create_background_task( self._poll_status_loop(), name=f"bluesound.poll_status_loop_{self.data.sync_status.id}", ) self.config_entry.async_on_unload(cancel_task(status_loop_task)) sync_status_loop_task = self.hass.async_create_background_task( self._poll_sync_status_loop(), name=f"bluesound.poll_sync_status_loop_{self.data.sync_status.id}", ) self.config_entry.async_on_unload(cancel_task(sync_status_loop_task)) presets_and_inputs_loop_task = self.hass.async_create_background_task( self._poll_presets_and_inputs_loop(), name=f"bluesound.poll_presets_and_inputs_loop_{self.data.sync_status.id}", ) self.config_entry.async_on_unload(cancel_task(presets_and_inputs_loop_task)) async def _async_update_data(self) -> BluesoundData: return self.data async def _poll_presets_and_inputs_loop(self) -> None: while True: await asyncio.sleep(PRESET_AND_INPUTS_INTERVAL.total_seconds()) try: preset = await self.player.presets() inputs = await self.player.inputs() self.async_set_updated_data( replace( self.data, presets=preset, inputs=inputs, ) ) except PlayerUnreachableError as ex: self.async_set_update_error(ex) except asyncio.CancelledError: return except Exception as ex: # noqa: BLE001 - this loop should never stop self.async_set_update_error(ex) async def _poll_status_loop(self) -> None: """Loop which polls the status of the player.""" while True: try: status = await self.player.status( etag=self.data.status.etag, poll_timeout=120, timeout=125 ) self.async_set_updated_data( replace( self.data, status=status, ) ) except PlayerUnreachableError as ex: self.async_set_update_error(ex) await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT.total_seconds()) except asyncio.CancelledError: return except Exception as ex: # noqa: BLE001 - this loop should never stop self.async_set_update_error(ex) await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT.total_seconds()) async def _poll_sync_status_loop(self) -> None: """Loop which polls the sync status of the player.""" while True: try: sync_status = await self.player.sync_status( etag=self.data.sync_status.etag, poll_timeout=120, timeout=125 ) self.async_set_updated_data( replace( self.data, sync_status=sync_status, ) ) except PlayerUnreachableError as ex: self.async_set_update_error(ex) await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT.total_seconds()) except asyncio.CancelledError: raise except Exception as ex: # noqa: BLE001 - this loop should never stop self.async_set_update_error(ex) await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT.total_seconds())