1 """Denon HEOS Media Player."""
3 from __future__
import annotations
6 from datetime
import timedelta
9 from pyheos
import Heos, HeosError, const
as heos_const
10 import voluptuous
as vol
19 async_dispatcher_connect,
20 async_dispatcher_send,
25 from .
import services
26 from .config_flow
import format_title
28 COMMAND_RETRY_ATTEMPTS,
30 DATA_CONTROLLER_MANAGER,
35 SIGNAL_HEOS_PLAYER_ADDED,
39 PLATFORMS = [Platform.MEDIA_PLAYER]
41 CONFIG_SCHEMA = vol.Schema(
43 cv.deprecated(DOMAIN),
44 {DOMAIN: vol.Schema({vol.Required(CONF_HOST): cv.string})},
46 extra=vol.ALLOW_EXTRA,
51 _LOGGER = logging.getLogger(__name__)
54 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
55 """Set up the HEOS component."""
56 if DOMAIN
not in config:
58 host = config[DOMAIN][CONF_HOST]
59 entries = hass.config_entries.async_entries(DOMAIN)
62 hass.async_create_task(
63 hass.config_entries.flow.async_init(
64 DOMAIN, context={
"source": SOURCE_IMPORT}, data={CONF_HOST: host}
70 if entry.data[CONF_HOST] != host:
71 hass.config_entries.async_update_entry(
72 entry, title=
format_title(host), data={**entry.data, CONF_HOST: host}
79 """Initialize config entry which represents the HEOS controller."""
81 if entry.unique_id
is None:
82 hass.config_entries.async_update_entry(entry, unique_id=DOMAIN)
84 host = entry.data[CONF_HOST]
87 controller = Heos(host, all_progress_events=
False)
89 await controller.connect(auto_reconnect=
True)
91 except HeosError
as error:
92 await controller.disconnect()
93 _LOGGER.debug(
"Unable to connect to controller %s: %s", host, error)
94 raise ConfigEntryNotReady
from error
97 async
def disconnect_controller(event):
98 await controller.disconnect()
100 entry.async_on_unload(
101 hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect_controller)
106 players = await controller.get_players()
108 if controller.is_signed_in:
109 favorites = await controller.get_favorites()
113 "%s is not logged in to a HEOS account and will be unable to"
114 " retrieve HEOS favorites: Use the 'heos.sign_in' service to"
115 " sign-in to a HEOS account"
119 inputs = await controller.get_input_sources()
120 except HeosError
as error:
121 await controller.disconnect()
122 _LOGGER.debug(
"Unable to retrieve players and sources: %s", error)
123 raise ConfigEntryNotReady
from error
126 await controller_manager.connect_listeners()
129 source_manager.connect_update(hass, controller)
133 hass.data[DOMAIN] = {
134 DATA_CONTROLLER_MANAGER: controller_manager,
135 DATA_GROUP_MANAGER: group_manager,
136 DATA_SOURCE_MANAGER: source_manager,
137 Platform.MEDIA_PLAYER: players,
140 DATA_ENTITY_ID_MAP: {},
143 services.register(hass, controller)
144 group_manager.connect_update()
145 entry.async_on_unload(group_manager.disconnect_update)
147 await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
153 """Unload a config entry."""
154 controller_manager = hass.data[DOMAIN][DATA_CONTROLLER_MANAGER]
155 await controller_manager.disconnect()
156 hass.data.pop(DOMAIN)
158 services.remove(hass)
160 return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
164 """Class that manages events of the controller."""
167 """Init the controller manager."""
175 """Subscribe to events of interest."""
188 heos_const.SIGNAL_HEOS_EVENT, self.
_heos_event_heos_event
193 """Disconnect subscriptions."""
194 for signal_remove
in self.
_signals_signals:
197 self.
controllercontroller.dispatcher.disconnect_all()
201 """Handle controller event."""
202 if event == heos_const.EVENT_PLAYERS_CHANGED:
203 self.
update_idsupdate_ids(data[heos_const.DATA_MAPPED_IDS])
208 """Handle connection event."""
209 if event == heos_const.EVENT_CONNECTED:
212 data = await self.
controllercontroller.load_players()
213 self.
update_idsupdate_ids(data[heos_const.DATA_MAPPED_IDS])
214 except HeosError
as ex:
215 _LOGGER.error(
"Unable to refresh players: %s", ex)
220 """Update the IDs in the device and entity registry."""
222 for new_id, old_id
in mapped_ids.items():
225 identifiers={(DOMAIN, old_id)}
227 new_identifiers = {(DOMAIN, new_id)}
230 entry.id, new_identifiers=new_identifiers
233 "Updated device %s identifiers to %s", entry.id, new_identifiers
237 Platform.MEDIA_PLAYER, DOMAIN,
str(old_id)
241 entity_id, new_unique_id=
str(new_id)
243 _LOGGER.debug(
"Updated entity %s unique id to %s", entity_id, new_id)
247 """Class that manages HEOS groups."""
250 """Init group manager."""
258 """Return mapping of all HeosMediaPlayer entity_ids to player_ids."""
259 return {v: k
for k, v
in self.
_hass_hass.data[DOMAIN][DATA_ENTITY_ID_MAP].items()}
262 """Return all group members for each player as entity_ids."""
263 group_info_by_entity_id = {
269 groups = await self.
controllercontroller.get_groups(refresh=
True)
270 except HeosError
as err:
271 _LOGGER.error(
"Unable to get HEOS group info: %s", err)
272 return group_info_by_entity_id
274 player_id_to_entity_id_map = self.
_hass_hass.data[DOMAIN][DATA_ENTITY_ID_MAP]
275 for group
in groups.values():
276 leader_entity_id = player_id_to_entity_id_map.get(group.leader.player_id)
277 member_entity_ids = [
278 player_id_to_entity_id_map[member.player_id]
279 for member
in group.members
280 if member.player_id
in player_id_to_entity_id_map
283 group_info = [leader_entity_id, *member_entity_ids]
285 group_info_by_entity_id[leader_entity_id] = group_info
286 for member_entity_id
in member_entity_ids:
287 group_info_by_entity_id[member_entity_id] = group_info
289 return group_info_by_entity_id
292 self, leader_entity_id: str, member_entity_ids: list[str]
294 """Create a group a group leader and member players."""
296 leader_id = entity_id_to_player_id_map.get(leader_entity_id)
299 f
"The group leader {leader_entity_id} could not be resolved to a HEOS"
303 entity_id_to_player_id_map[member]
304 for member
in member_entity_ids
305 if member
in entity_id_to_player_id_map
309 await self.
controllercontroller.create_group(leader_id, member_ids)
310 except HeosError
as err:
312 "Failed to group %s with %s: %s",
319 """Remove `player_entity_id` from any group."""
323 f
"The player {player_entity_id} could not be resolved to a HEOS player."
327 await self.
controllercontroller.create_group(player_id, [])
328 except HeosError
as err:
330 "Failed to ungroup %s: %s",
336 """Update the group membership from the controller."""
338 heos_const.EVENT_GROUPS_CHANGED,
339 heos_const.EVENT_CONNECTED,
340 SIGNAL_HEOS_PLAYER_ADDED,
344 _LOGGER.debug(
"Groups updated due to change event")
348 _LOGGER.debug(
"Groups empty")
351 """Connect listener for when groups change and signal player update."""
360 async
def _async_handle_player_added():
364 len(self.
_hass_hass.data[DOMAIN][Platform.MEDIA_PLAYER])
365 <= len(self.
_hass_hass.data[DOMAIN][DATA_ENTITY_ID_MAP])
372 self.
_hass_hass, SIGNAL_HEOS_PLAYER_ADDED, _async_handle_player_added
377 """Disconnect the listeners."""
384 """Provide access to group members for player entities."""
389 """Class that manages sources for players."""
396 retry_delay: int = COMMAND_RETRY_DELAY,
397 max_retry_attempts: int = COMMAND_RETRY_ATTEMPTS,
399 """Init input manager."""
407 """Build a single list of inputs from various types."""
409 source_list.extend([favorite.name
for favorite
in self.
favoritesfavorites.values()])
410 source_list.extend([source.name
for source
in self.
inputsinputs])
414 """Determine type of source and play it."""
418 for index, favorite
in self.
favoritesfavorites.items()
419 if favorite.name == source
423 if index
is not None:
424 await player.play_favorite(index)
430 for input_source
in self.
inputsinputs
431 if input_source.name == source
435 if input_source
is not None:
436 await player.play_input_source(input_source)
439 _LOGGER.error(
"Unknown source: %s", source)
442 """Determine current source from now playing media."""
444 if now_playing_media.source_id == heos_const.MUSIC_SOURCE_AUX_INPUT:
448 for input_source
in self.
inputsinputs
449 if input_source.input_name == now_playing_media.media_id
457 for source
in self.
favoritesfavorites.values()
458 if source.name == now_playing_media.station
459 or source.media_id == now_playing_media.album_id
465 """Connect listener for when sources change and signal player update.
467 EVENT_SOURCES_CHANGED is often raised multiple times in response to a
468 physical event therefore throttle it. Retrieving sources immediately
469 after the event may fail so retry.
472 @Throttle(MIN_UPDATE_SOURCES)
473 async
def get_sources():
478 if controller.is_signed_in:
479 favorites = await controller.get_favorites()
480 inputs = await controller.get_input_sources()
481 except HeosError
as error:
485 "Error retrieving sources and will retry: %s", error
489 _LOGGER.error(
"Unable to update sources: %s", error)
492 return favorites, inputs
494 async
def update_sources(event, data=None):
496 heos_const.EVENT_SOURCES_CHANGED,
497 heos_const.EVENT_USER_CHANGED,
498 heos_const.EVENT_CONNECTED,
501 if sources := await get_sources():
504 _LOGGER.debug(
"Sources updated due to changed event")
508 controller.dispatcher.connect(
509 heos_const.SIGNAL_CONTROLLER_EVENT, update_sources
511 controller.dispatcher.connect(heos_const.SIGNAL_HEOS_EVENT, update_sources)
def update_ids(self, dict[int, int] mapped_ids)
def _controller_event(self, event, data)
def _heos_event(self, event)
def connect_listeners(self)
def __init__(self, hass, controller)
def async_get_group_membership(self)
def __init__(self, hass, controller)
def disconnect_update(self)
def async_update_groups(self, event, data=None)
def group_membership(self)
dict _get_entity_id_to_player_id_map(self)
None async_join_players(self, str leader_entity_id, list[str] member_entity_ids)
def async_unjoin_player(self, str player_entity_id)
def play_source(self, str source, player)
def connect_update(self, hass, controller)
None __init__(self, favorites, inputs, *int retry_delay=COMMAND_RETRY_DELAY, int max_retry_attempts=COMMAND_RETRY_ATTEMPTS)
def _build_source_list(self)
def get_current_source(self, now_playing_media)
None async_update_device(HomeAssistant hass, ConfigEntry entry, str adapter, AdapterDetails details)
web.Response get(self, web.Request request, str config_key)
str format_title(str host)
bool async_setup(HomeAssistant hass, ConfigType config)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
None async_update_entity(HomeAssistant hass, str entity_id)