# ty:ignore[unresolved-import] """Initialize component.""" from __future__ import annotations import asyncio from dataclasses import dataclass from typing import TYPE_CHECKING from homeassistant.components import websocket_api from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.issue_registry import ( IssueSeverity, async_create_issue, async_delete_issue, ) from music_assistant_client import MusicAssistantClient from music_assistant_client.exceptions import ( CannotConnect, InvalidServerVersion, MusicAssistantClientException, ) from music_assistant_models.errors import ( ActionUnavailable, AuthenticationFailed, AuthenticationRequired, InvalidToken, MusicAssistantError, ) from .actions import ( MassQueueActions, get_music_assistant_client, setup_controller_and_actions, ) from .const import CONF_TOKEN, DOMAIN, LOGGER from .services import register_actions from .websocket_commands import ( api_download_and_encode_image, api_download_images, api_get_entity_info, api_get_user_info, ) if TYPE_CHECKING: from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType, Event PLATFORMS = [] CONNECT_TIMEOUT = 10 LISTEN_READY_TIMEOUT = 30 CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) type MusicAssistantConfigEntry = ConfigEntry[MusicAssistantQueueEntryData] @dataclass class MusicAssistantQueueEntryData: """Hold Mass data for the config entry.""" mass: MusicAssistantClient actions: MassQueueActions listen_task: asyncio.Task async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: ARG001 """Set up the Music Assistant component.""" return True async def async_setup_entry( # noqa: PLR0915 hass: HomeAssistant, entry: MusicAssistantConfigEntry, ) -> bool: """Set up Music Assistant from a config entry.""" http_session = async_get_clientsession(hass, verify_ssl=False) mass_url = entry.data[CONF_URL] # Get token from config entry (for schema >= AUTH_SCHEMA_VERSION) token = entry.data.get(CONF_TOKEN) mass = MusicAssistantClient(mass_url, http_session, token=token) try: async with asyncio.timeout(CONNECT_TIMEOUT): await mass.connect() except (TimeoutError, CannotConnect) as err: exc = f"Failed to connect to music assistant server {mass_url}" raise ConfigEntryNotReady( exc, ) from err except InvalidServerVersion as err: async_create_issue( hass, DOMAIN, "invalid_server_version", is_fixable=False, severity=IssueSeverity.ERROR, translation_key="invalid_server_version", ) exc = f"Invalid server version: {err}" raise ConfigEntryNotReady(exc) from err except (AuthenticationRequired, AuthenticationFailed, InvalidToken) as err: exc = f"Authentication failed for {mass_url}: {err}" raise ConfigEntryAuthFailed(exc) from err except MusicAssistantClientException as err: exc = f"Failed to connect to music assistant server {mass_url}: {err}" raise ConfigEntryNotReady(exc) from err except MusicAssistantError as err: LOGGER.exception("Failed to connect to music assistant server", exc_info=err) exc = f"Unknown error connecting to the Music Assistant server {mass_url}" raise ConfigEntryNotReady( exc, ) from err async_delete_issue(hass, DOMAIN, "invalid_server_version") async def on_hass_stop(event: Event) -> None: # noqa: ARG001 """Handle incoming stop event from Home Assistant.""" await mass.disconnect() entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop), ) # launch the music assistant client listen task in the background # use the init_ready event to wait until initialization is done init_ready = asyncio.Event() listen_task = asyncio.create_task(_client_listen(hass, entry, mass, init_ready)) try: async with asyncio.timeout(LISTEN_READY_TIMEOUT): await init_ready.wait() except TimeoutError as err: listen_task.cancel() exc = "Music Assistant client not ready" raise ConfigEntryNotReady(exc) from err # store the listen task and mass client in the entry data actions = await setup_controller_and_actions(hass, mass, entry) register_actions(hass) entry.runtime_data = MusicAssistantQueueEntryData(mass, actions, listen_task) websocket_api.async_register_command(hass, api_download_images) websocket_api.async_register_command(hass, api_download_and_encode_image) websocket_api.async_register_command(hass, api_get_entity_info) websocket_api.async_register_command(hass, api_get_user_info) # If the listen task is already failed, we need to raise ConfigEntryNotReady if listen_task.done() and (listen_error := listen_task.exception()) is not None: await hass.config_entries.async_unload_platforms(entry, PLATFORMS) try: await mass.disconnect() finally: raise ConfigEntryNotReady(listen_error) from listen_error # initialize platforms await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True async def _client_listen( hass: HomeAssistant, entry: ConfigEntry, mass: MusicAssistantClient, init_ready: asyncio.Event, ) -> None: """Listen with the client.""" try: await mass.start_listening(init_ready) except MusicAssistantError as err: if entry.state != ConfigEntryState.LOADED: raise LOGGER.error("Failed to listen: %s", err) except Exception as err: # pylint: disable=broad-except # We need to guard against unknown exceptions to not crash this task. if entry.state != ConfigEntryState.LOADED: raise LOGGER.exception("Unexpected exception: %s", err) if not hass.is_stopping: LOGGER.debug("Disconnected from server. Reloading integration") hass.async_create_task(hass.config_entries.async_reload(entry.entry_id)) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: mass_entry_data: MusicAssistantQueueEntryData = entry.runtime_data mass_entry_data.listen_task.cancel() await mass_entry_data.mass.disconnect() return unload_ok async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry, ) -> bool: """Remove a config entry from a device.""" player_id = next( ( identifier[1] for identifier in device_entry.identifiers if identifier[0] == DOMAIN ), None, ) if player_id is None: # this should not be possible at all, but guard it anyways return False mass = get_music_assistant_client(hass, config_entry.entry_id) if mass.players.get(player_id) is None: # player is already removed on the server, this is an orphaned device return True # try to remove the player from the server try: await mass.config.remove_player_config(player_id) except ActionUnavailable: return False else: return True