Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Music Assistant (music-assistant.io) integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from dataclasses import dataclass
7 from typing import TYPE_CHECKING
8 
9 from music_assistant_client import MusicAssistantClient
10 from music_assistant_client.exceptions import CannotConnect, InvalidServerVersion
11 from music_assistant_models.enums import EventType
12 from music_assistant_models.errors import MusicAssistantError
13 
14 from homeassistant.config_entries import ConfigEntry, ConfigEntryState
15 from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP, Platform
16 from homeassistant.core import Event, HomeAssistant
17 from homeassistant.exceptions import ConfigEntryNotReady
18 from homeassistant.helpers import device_registry as dr
19 from homeassistant.helpers.aiohttp_client import async_get_clientsession
21  IssueSeverity,
22  async_create_issue,
23  async_delete_issue,
24 )
25 
26 from .const import DOMAIN, LOGGER
27 
28 if TYPE_CHECKING:
29  from music_assistant_models.event import MassEvent
30 
31 PLATFORMS = [Platform.MEDIA_PLAYER]
32 
33 CONNECT_TIMEOUT = 10
34 LISTEN_READY_TIMEOUT = 30
35 
36 type MusicAssistantConfigEntry = ConfigEntry[MusicAssistantEntryData]
37 
38 
39 @dataclass
41  """Hold Mass data for the config entry."""
42 
43  mass: MusicAssistantClient
44  listen_task: asyncio.Task
45 
46 
48  hass: HomeAssistant, entry: MusicAssistantConfigEntry
49 ) -> bool:
50  """Set up Music Assistant from a config entry."""
51  http_session = async_get_clientsession(hass, verify_ssl=False)
52  mass_url = entry.data[CONF_URL]
53  mass = MusicAssistantClient(mass_url, http_session)
54 
55  try:
56  async with asyncio.timeout(CONNECT_TIMEOUT):
57  await mass.connect()
58  except (TimeoutError, CannotConnect) as err:
59  raise ConfigEntryNotReady(
60  f"Failed to connect to music assistant server {mass_url}"
61  ) from err
62  except InvalidServerVersion as err:
64  hass,
65  DOMAIN,
66  "invalid_server_version",
67  is_fixable=False,
68  severity=IssueSeverity.ERROR,
69  translation_key="invalid_server_version",
70  )
71  raise ConfigEntryNotReady(f"Invalid server version: {err}") from err
72  except MusicAssistantError as err:
73  LOGGER.exception("Failed to connect to music assistant server", exc_info=err)
74  raise ConfigEntryNotReady(
75  f"Unknown error connecting to the Music Assistant server {mass_url}"
76  ) from err
77 
78  async_delete_issue(hass, DOMAIN, "invalid_server_version")
79 
80  async def on_hass_stop(event: Event) -> None:
81  """Handle incoming stop event from Home Assistant."""
82  await mass.disconnect()
83 
84  entry.async_on_unload(
85  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
86  )
87 
88  # launch the music assistant client listen task in the background
89  # use the init_ready event to wait until initialization is done
90  init_ready = asyncio.Event()
91  listen_task = asyncio.create_task(_client_listen(hass, entry, mass, init_ready))
92 
93  try:
94  async with asyncio.timeout(LISTEN_READY_TIMEOUT):
95  await init_ready.wait()
96  except TimeoutError as err:
97  listen_task.cancel()
98  raise ConfigEntryNotReady("Music Assistant client not ready") from err
99 
100  # store the listen task and mass client in the entry data
101  entry.runtime_data = MusicAssistantEntryData(mass, listen_task)
102 
103  # If the listen task is already failed, we need to raise ConfigEntryNotReady
104  if listen_task.done() and (listen_error := listen_task.exception()) is not None:
105  await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
106  try:
107  await mass.disconnect()
108  finally:
109  raise ConfigEntryNotReady(listen_error) from listen_error
110 
111  # initialize platforms
112  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
113 
114  # register listener for removed players
115  async def handle_player_removed(event: MassEvent) -> None:
116  """Handle Mass Player Removed event."""
117  if event.object_id is None:
118  return
119  dev_reg = dr.async_get(hass)
120  if hass_device := dev_reg.async_get_device({(DOMAIN, event.object_id)}):
121  dev_reg.async_update_device(
122  hass_device.id, remove_config_entry_id=entry.entry_id
123  )
124 
125  entry.async_on_unload(
126  mass.subscribe(handle_player_removed, EventType.PLAYER_REMOVED)
127  )
128 
129  return True
130 
131 
132 async def _client_listen(
133  hass: HomeAssistant,
134  entry: ConfigEntry,
135  mass: MusicAssistantClient,
136  init_ready: asyncio.Event,
137 ) -> None:
138  """Listen with the client."""
139  try:
140  await mass.start_listening(init_ready)
141  except MusicAssistantError as err:
142  if entry.state != ConfigEntryState.LOADED:
143  raise
144  LOGGER.error("Failed to listen: %s", err)
145  except Exception as err: # pylint: disable=broad-except
146  # We need to guard against unknown exceptions to not crash this task.
147  if entry.state != ConfigEntryState.LOADED:
148  raise
149  LOGGER.exception("Unexpected exception: %s", err)
150 
151  if not hass.is_stopping:
152  LOGGER.debug("Disconnected from server. Reloading integration")
153  hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
154 
155 
156 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
157  """Unload a config entry."""
158  unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
159 
160  if unload_ok:
161  mass_entry_data: MusicAssistantEntryData = entry.runtime_data
162  mass_entry_data.listen_task.cancel()
163  await mass_entry_data.mass.disconnect()
164 
165  return unload_ok
None _client_listen(HomeAssistant hass, ConfigEntry entry, MusicAssistantClient mass, asyncio.Event init_ready)
Definition: __init__.py:137
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:156
bool async_setup_entry(HomeAssistant hass, MusicAssistantConfigEntry entry)
Definition: __init__.py:49
None async_create_issue(HomeAssistant hass, str entry_id)
Definition: repairs.py:69
None async_delete_issue(HomeAssistant hass, str entry_id)
Definition: repairs.py:85
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)