1 """MediaPlayer platform for Roon integration."""
3 from __future__
import annotations
6 from typing
import Any, cast
8 from roonapi
import split_media_path
9 import voluptuous
as vol
14 MediaPlayerEntityFeature,
25 async_dispatcher_connect,
26 async_dispatcher_send,
32 from .const
import DOMAIN
33 from .media_browser
import browse_media
35 _LOGGER = logging.getLogger(__name__)
37 SERVICE_TRANSFER =
"transfer"
39 ATTR_TRANSFER =
"transfer_id"
41 REPEAT_MODE_MAPPING_TO_HA = {
42 "loop": RepeatMode.ALL,
43 "disabled": RepeatMode.OFF,
44 "loop_one": RepeatMode.ONE,
47 REPEAT_MODE_MAPPING_TO_ROON = {
48 value: key
for key, value
in REPEAT_MODE_MAPPING_TO_HA.items()
54 config_entry: ConfigEntry,
55 async_add_entities: AddEntitiesCallback,
57 """Set up Roon MediaPlayer from Config Entry."""
58 roon_server = hass.data[DOMAIN][config_entry.entry_id]
62 platform = entity_platform.async_get_current_platform()
63 platform.async_register_entity_service(
65 {vol.Required(ATTR_TRANSFER): cv.entity_id},
70 def async_update_media_player(player_data):
71 """Add or update Roon MediaPlayer."""
72 dev_id = player_data[
"dev_id"]
73 if dev_id
not in media_players:
75 media_player =
RoonDevice(roon_server, player_data)
76 media_players.add(dev_id)
81 hass, f
"room_media_player_update_{dev_id}", player_data
89 """Representation of an Roon device."""
91 _attr_should_poll =
False
92 _attr_supported_features = (
93 MediaPlayerEntityFeature.BROWSE_MEDIA
94 | MediaPlayerEntityFeature.GROUPING
95 | MediaPlayerEntityFeature.PAUSE
96 | MediaPlayerEntityFeature.STOP
97 | MediaPlayerEntityFeature.PREVIOUS_TRACK
98 | MediaPlayerEntityFeature.NEXT_TRACK
99 | MediaPlayerEntityFeature.REPEAT_SET
100 | MediaPlayerEntityFeature.SHUFFLE_SET
101 | MediaPlayerEntityFeature.SEEK
102 | MediaPlayerEntityFeature.TURN_ON
103 | MediaPlayerEntityFeature.TURN_OFF
104 | MediaPlayerEntityFeature.VOLUME_MUTE
105 | MediaPlayerEntityFeature.PLAY
106 | MediaPlayerEntityFeature.PLAY_MEDIA
110 """Initialize Roon device object."""
131 """Register callback."""
135 f
"room_media_player_update_{self.unique_id}",
143 """Handle device updates."""
149 """Return the grouped players."""
151 roon_names = self.
_server_server.roonapi.grouped_zone_names(self.
_output_id_output_id)
152 return [self.
_server_server.
entity_id(roon_name)
for roon_name
in roon_names]
156 """Return the device info."""
160 dev_model = self.
player_dataplayer_data[
"source_controls"][0].
get(
"display_name")
162 identifiers={(DOMAIN, self.
unique_idunique_id)},
166 name=cast(str |
None, self.
namename),
167 manufacturer=
"RoonLabs",
169 via_device=(DOMAIN, self.
_server_server.roon_id),
173 """Update session object."""
176 if not self.
player_dataplayer_data[
"is_available"]:
189 """Parse volume data to determine volume levels and mute state."""
195 "incremental":
False,
199 volume_data = player_data[
"volume"]
203 volume[
"fixed"] =
False
204 volume[
"incremental"] = volume_data[
"type"] ==
"incremental"
205 volume[
"muted"] = volume_data.get(
"is_muted",
False)
206 volume[
"step"] = convert(volume_data.get(
"step"), int, 0)
209 volume_max = volume_data[
"max"]
210 volume_min = volume_data[
"min"]
212 raw_level = convert(volume_data[
"value"], float, 0)
214 volume_range = volume_max - volume_min
215 volume_percentage_factor = volume_range / 100
217 level = (raw_level - volume_min) / volume_percentage_factor
218 volume[
"level"] = round(level) / 100
225 """Parse now playing data to determine title, artist, position, duration and artwork."""
234 now_playing_data =
None
236 media_position = convert(player_data.get(
"seek_position"), int, 0)
239 now_playing_data = player_data[
"now_playing"]
240 media_title = now_playing_data[
"three_line"][
"line1"]
241 media_artist = now_playing_data[
"three_line"][
"line2"]
242 media_album_name = now_playing_data[
"three_line"][
"line3"]
243 media_duration = convert(now_playing_data.get(
"length"), int, 0)
244 image_id = now_playing_data.get(
"image_key")
249 now_playing[
"title"] = media_title
250 now_playing[
"artist"] = media_artist
251 now_playing[
"album"] = media_album_name
252 now_playing[
"position"] = media_position
253 now_playing[
"duration"] = media_duration
255 now_playing[
"image"] = self.
_server_server.roonapi.get_image(image_id)
260 """Update the power state and player state."""
264 if "source_controls" in self.
player_dataplayer_data:
265 for source
in self.
player_dataplayer_data[
"source_controls"]:
266 if source[
"supports_standby"]
and source[
"status"] !=
"indeterminate":
268 if source[
"status"]
in [
"standby",
"deselected"]:
269 new_state = MediaPlayerState.OFF
275 or self.
player_dataplayer_data[
"state"] ==
"loading"
277 new_state = MediaPlayerState.PLAYING
278 elif self.
player_dataplayer_data[
"state"] ==
"stopped":
279 new_state = MediaPlayerState.IDLE
280 elif self.
player_dataplayer_data[
"state"] ==
"paused":
281 new_state = MediaPlayerState.PAUSED
283 new_state = MediaPlayerState.IDLE
294 volume = RoonDevice._parse_volume(self.
player_dataplayer_data)
319 """Return current session Id."""
324 """Return current session Id."""
329 """Album artist of current playing media (Music track only)."""
334 """Return power state of source controls."""
338 """Send play command to device."""
339 self.
_server_server.roonapi.playback_control(self.
output_idoutput_id,
"play")
342 """Send pause command to device."""
343 self.
_server_server.roonapi.playback_control(self.
output_idoutput_id,
"pause")
346 """Toggle play command to device."""
347 self.
_server_server.roonapi.playback_control(self.
output_idoutput_id,
"playpause")
350 """Send stop command to device."""
351 self.
_server_server.roonapi.playback_control(self.
output_idoutput_id,
"stop")
354 """Send next track command to device."""
355 self.
_server_server.roonapi.playback_control(self.
output_idoutput_id,
"next")
358 """Send previous track command to device."""
359 self.
_server_server.roonapi.playback_control(self.
output_idoutput_id,
"previous")
362 """Send seek command to device."""
369 """Send new volume_level to device."""
370 volume = volume * 100
371 self.
_server_server.roonapi.set_volume_percent(self.
output_idoutput_id, volume)
374 """Send mute/unmute to device."""
378 """Send new volume_level to device."""
380 self.
_server_server.roonapi.change_volume_raw(self.
output_idoutput_id, 1,
"relative")
382 self.
_server_server.roonapi.change_volume_percent(self.
output_idoutput_id, 3)
385 """Send new volume_level to device."""
387 self.
_server_server.roonapi.change_volume_raw(self.
output_idoutput_id, -1,
"relative")
389 self.
_server_server.roonapi.change_volume_percent(self.
output_idoutput_id, -3)
392 """Turn on device (if supported)."""
396 for source
in self.
player_dataplayer_data[
"source_controls"]:
397 if source[
"supports_standby"]
and source[
"status"] !=
"indeterminate":
398 self.
_server_server.roonapi.convenience_switch(
399 self.
output_idoutput_id, source[
"control_key"]
404 """Turn off device (if supported)."""
409 for source
in self.
player_dataplayer_data[
"source_controls"]:
410 if source[
"supports_standby"]
and source[
"status"] !=
"indeterminate":
411 self.
_server_server.roonapi.standby(self.
output_idoutput_id, source[
"control_key"])
415 """Set shuffle state."""
419 """Set repeat mode."""
420 if repeat
not in REPEAT_MODE_MAPPING_TO_ROON:
421 raise ValueError(f
"Unsupported repeat mode: {repeat}")
422 self.
_server_server.roonapi.repeat(self.
output_idoutput_id, REPEAT_MODE_MAPPING_TO_ROON[repeat])
425 self, media_type: MediaType | str, media_id: str, **kwargs: Any
427 """Send the play_media command to the media player."""
429 _LOGGER.debug(
"Playback request for %s / %s", media_type, media_id)
430 if media_type
in (
"library",
"track"):
432 self.
_server_server.roonapi.play_id(self.
zone_idzone_id, media_id)
435 path_list = split_media_path(media_id)
436 if not self.
_server_server.roonapi.play_media(self.
zone_idzone_id, path_list):
438 "Playback request for %s / %s / %s was unsuccessful",
445 """Join `group_members` as a player group with the current player."""
447 zone_data = self.
_server_server.roonapi.zone_by_output_id(self.
_output_id_output_id)
448 if zone_data
is None:
449 _LOGGER.error(
"No zone data for %s", self.
namename)
453 for zone
in self.
_server_server.zones.values():
454 for output
in zone[
"outputs"]:
456 zone[
"display_name"] != self.
namename
457 and output[
"output_id"]
458 in self.
player_dataplayer_data[
"can_group_with_output_ids"]
459 and zone[
"display_name"]
not in sync_available
461 sync_available[zone[
"display_name"]] = output[
"output_id"]
464 for entity_id
in group_members:
465 name = self.
_server_server.roon_name(entity_id)
467 _LOGGER.error(
"No roon player found for %s", entity_id)
469 if name
not in sync_available:
472 "Can't join player %s with %s because it's not in the join"
477 list(sync_available),
482 _LOGGER.debug(
"Joining %s to %s", names, self.
namename)
483 self.
_server_server.roonapi.group_outputs(
484 [self.
_output_id_output_id] + [sync_available[name]
for name
in names]
488 """Remove this player from any group."""
492 "Can't unjoin player %s because it's not in a group",
500 """Transfer playback from this roon player to another."""
502 name = self.
_server_server.roon_name(transfer_id)
504 _LOGGER.error(
"No roon player found for %s", transfer_id)
508 output[
"display_name"]: output[
"zone_id"]
509 for output
in self.
_server_server.zones.values()
510 if output[
"display_name"] != self.
namename
513 if (transfer_id := zone_ids.get(name))
is None:
515 "Can't transfer from %s to %s because destination is not known %s",
521 _LOGGER.debug(
"Transferring from %s to %s", self.
namename, name)
522 await self.
hasshass.async_add_executor_job(
523 self.
_server_server.roonapi.transfer_zone, self.
_zone_id_zone_id, transfer_id
528 media_content_type: MediaType | str |
None =
None,
529 media_content_id: str |
None =
None,
531 """Implement the websocket media browsing helper."""
532 return await self.
hasshass.async_add_executor_job(
None async_write_ha_state(self)
None async_on_remove(self, CALLBACK_TYPE func)
None schedule_update_ha_state(self, bool force_refresh=False)
str|UndefinedType|None name(self)
web.Response get(self, web.Request request, str config_key)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)