Files
2026-07-17 10:12:15 -04:00

231 lines
7.7 KiB
Python

# 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