1 """Support for interfacing to the SqueezeBox API."""
3 from __future__
import annotations
5 from collections.abc
import Callable
6 from datetime
import datetime
9 from typing
import TYPE_CHECKING, Any
11 from pysqueezebox
import Server, async_discover
12 import voluptuous
as vol
21 MediaPlayerEntityFeature,
25 async_process_play_media_url,
32 config_validation
as cv,
35 entity_registry
as er,
38 CONNECTION_NETWORK_MAC,
48 from .browse_media
import (
52 media_source_content_filter,
59 SIGNAL_PLAYER_DISCOVERED,
60 SQUEEZEBOX_SOURCE_STRINGS,
62 from .coordinator
import SqueezeBoxPlayerUpdateCoordinator
65 from .
import SqueezeboxConfigEntry
67 SERVICE_CALL_METHOD =
"call_method"
68 SERVICE_CALL_QUERY =
"call_query"
70 ATTR_QUERY_RESULT =
"query_result"
72 _LOGGER = logging.getLogger(__name__)
75 ATTR_PARAMETERS =
"parameters"
76 ATTR_OTHER_PLAYER =
"other_player"
83 "pause": MediaPlayerState.PAUSED,
84 "play": MediaPlayerState.PLAYING,
85 "stop": MediaPlayerState.IDLE,
90 """Start a server discovery task."""
92 def _discovered_server(server: Server) ->
None:
93 discovery_flow.async_create_flow(
96 context={
"source": SOURCE_INTEGRATION_DISCOVERY},
98 CONF_HOST: server.host,
99 CONF_PORT:
int(server.port),
104 hass.data.setdefault(DOMAIN, {})
105 if DISCOVERY_TASK
not in hass.data[DOMAIN]:
106 _LOGGER.debug(
"Adding server discovery task for squeezebox")
107 hass.data[DOMAIN][DISCOVERY_TASK] = hass.async_create_background_task(
109 name=
"squeezebox server discovery",
115 entry: SqueezeboxConfigEntry,
116 async_add_entities: AddEntitiesCallback,
118 """Set up the Squeezebox media_player platform from a server config entry."""
121 async
def _player_discovered(player: SqueezeBoxPlayerUpdateCoordinator) ->
None:
122 _LOGGER.debug(
"Setting up media_player entity for player %s", player)
125 entry.async_on_unload(
130 platform = entity_platform.async_get_current_platform()
131 platform.async_register_entity_service(
134 vol.Required(ATTR_COMMAND): cv.string,
135 vol.Optional(ATTR_PARAMETERS): vol.All(
136 cv.ensure_list, vol.Length(min=1), [cv.string]
141 platform.async_register_entity_service(
144 vol.Required(ATTR_COMMAND): cv.string,
145 vol.Optional(ATTR_PARAMETERS): vol.All(
146 cv.ensure_list, vol.Length(min=1), [cv.string]
153 entry.async_on_unload(
async_at_start(hass, start_server_discovery))
157 CoordinatorEntity[SqueezeBoxPlayerUpdateCoordinator], MediaPlayerEntity
159 """Representation of the media player features of a SqueezeBox device.
161 Wraps a pysqueezebox.Player() object.
164 _attr_supported_features = (
165 MediaPlayerEntityFeature.BROWSE_MEDIA
166 | MediaPlayerEntityFeature.PAUSE
167 | MediaPlayerEntityFeature.VOLUME_SET
168 | MediaPlayerEntityFeature.VOLUME_MUTE
169 | MediaPlayerEntityFeature.PREVIOUS_TRACK
170 | MediaPlayerEntityFeature.NEXT_TRACK
171 | MediaPlayerEntityFeature.SEEK
172 | MediaPlayerEntityFeature.TURN_ON
173 | MediaPlayerEntityFeature.TURN_OFF
174 | MediaPlayerEntityFeature.PLAY_MEDIA
175 | MediaPlayerEntityFeature.PLAY
176 | MediaPlayerEntityFeature.REPEAT_SET
177 | MediaPlayerEntityFeature.SHUFFLE_SET
178 | MediaPlayerEntityFeature.CLEAR_PLAYLIST
179 | MediaPlayerEntityFeature.STOP
180 | MediaPlayerEntityFeature.GROUPING
181 | MediaPlayerEntityFeature.MEDIA_ENQUEUE
183 _attr_has_entity_name =
True
185 _last_update: datetime |
None =
None
189 coordinator: SqueezeBoxPlayerUpdateCoordinator,
191 """Initialize the SqueezeBox device."""
193 player = coordinator.player
200 if player.model ==
"SqueezeLite" or "SqueezePlay" in player.model:
201 _manufacturer =
"Ralph Irving"
203 "Squeezebox" in player.model
204 or "Transporter" in player.model
205 or "Slim" in player.model
207 _manufacturer =
"Logitech"
212 connections={(CONNECTION_NETWORK_MAC, self.
_attr_unique_id_attr_unique_id)},
213 via_device=(DOMAIN, coordinator.server_uuid),
215 manufacturer=_manufacturer,
220 """Handle updated data from the coordinator."""
228 """Return True if entity is available."""
229 return self.coordinator.available
and super().available
233 """Return device-specific attributes."""
235 attr: getattr(self, attr)
236 for attr
in ATTR_TO_PROPERTY
237 if getattr(self, attr)
is not None
241 def state(self) -> MediaPlayerState | None:
242 """Return the state of the device."""
243 if not self.
_player_player.power:
244 return MediaPlayerState.OFF
245 if self.
_player_player.mode
and self.
_player_player.mode
in SQUEEZEBOX_MODE:
246 return SQUEEZEBOX_MODE[self.
_player_player.mode]
248 "Received unknown mode %s from player %s", self.
_player_player.mode, self.
namenamename
253 """Remove from list of known players when removed from hass."""
254 known_servers = self.
hasshasshass.data[DOMAIN][KNOWN_SERVERS]
255 known_players = known_servers[self.coordinator.server_uuid][KNOWN_PLAYERS]
256 known_players.remove(self.coordinator.player.player_id)
260 """Volume level of the media player (0..1)."""
268 """Return true if volume is muted."""
273 """Content ID of current playing media."""
274 if not self.
_player_player.playlist:
276 if len(self.
_player_player.playlist) > 1:
277 urls = [{
"url": track[
"url"]}
for track
in self.
_player_player.playlist]
278 return json.dumps({
"index": self.
_player_player.current_index,
"urls": urls})
283 """Content type of current playing media."""
284 if not self.
_player_player.playlist:
286 if len(self.
_player_player.playlist) > 1:
287 return MediaType.PLAYLIST
288 return MediaType.MUSIC
292 """Duration of current playing media in seconds."""
297 """Position of current playing media in seconds."""
302 """Last time status was updated."""
307 """Image url of current playing media."""
308 return str(self.
_player_player.image_url)
if self.
_player_player.image_url
else None
312 """Title of current playing media."""
317 """Channel (e.g. webradio name) of current playing media."""
322 """Artist of current playing media."""
327 """Album of current playing media."""
332 """Repeat setting."""
333 if self.
_player_player.repeat ==
"song":
334 return RepeatMode.ONE
335 if self.
_player_player.repeat ==
"playlist":
336 return RepeatMode.ALL
337 return RepeatMode.OFF
341 """Boolean if shuffle is enabled."""
347 """List players we are synced with."""
348 ent_reg = er.async_get(self.
hasshasshass)
351 for player
in self.
_player_player.sync_group
353 entity_id := ent_reg.async_get_entity_id(
354 Platform.MEDIA_PLAYER, DOMAIN, player
361 """Return the result from the call_query service."""
365 """Turn off media player."""
366 await self.
_player_player.async_set_power(
False)
370 """Volume up media player."""
371 await self.
_player_player.async_set_volume(
"+5")
375 """Volume down media player."""
376 await self.
_player_player.async_set_volume(
"-5")
380 """Set volume level, range 0..1."""
381 volume_percent =
str(
int(volume * 100))
382 await self.
_player_player.async_set_volume(volume_percent)
386 """Mute (true) or unmute (false) media player."""
387 await self.
_player_player.async_set_muting(mute)
391 """Send stop command to media player."""
396 """Send pause command to media player."""
397 await self.
_player_player.async_toggle_pause()
401 """Send play command to media player."""
402 await self.
_player_player.async_play()
406 """Send pause command to media player."""
407 await self.
_player_player.async_pause()
411 """Send next track command."""
412 await self.
_player_player.async_index(
"+1")
416 """Send next track command."""
417 await self.
_player_player.async_index(
"-1")
421 """Send seek command."""
422 await self.
_player_player.async_time(position)
426 """Turn the media player on."""
427 await self.
_player_player.async_set_power(
True)
431 self, media_type: MediaType | str, media_id: str, **kwargs: Any
433 """Send the play_media command to the media player."""
436 enqueue: MediaPlayerEnqueue |
None = kwargs.get(ATTR_MEDIA_ENQUEUE)
438 if enqueue == MediaPlayerEnqueue.ADD:
440 elif enqueue == MediaPlayerEnqueue.NEXT:
442 elif enqueue == MediaPlayerEnqueue.PLAY:
447 if media_source.is_media_source_id(media_id):
448 media_type = MediaType.MUSIC
449 play_item = await media_source.async_resolve_media(
452 media_id = play_item.url
454 if media_type
in MediaType.MUSIC:
455 if not media_id.startswith(SQUEEZEBOX_SOURCE_STRINGS):
459 await self.
_player_player.async_load_url(media_id, cmd)
462 if media_type == MediaType.PLAYLIST:
466 "search_id": media_id,
467 "search_type": MediaType.PLAYLIST,
472 content = json.loads(media_id)
473 playlist = content[
"urls"]
474 index = content[
"index"]
477 "search_id": media_id,
478 "search_type": media_type,
482 _LOGGER.debug(
"Generated playlist: %s", playlist)
484 await self.
_player_player.async_load_playlist(playlist, cmd)
485 if index
is not None:
486 await self.
_player_player.async_index(index)
490 """Set the repeat mode."""
491 if repeat == RepeatMode.ALL:
492 repeat_mode =
"playlist"
493 elif repeat == RepeatMode.ONE:
502 """Enable/disable shuffle mode."""
503 shuffle_mode =
"song" if shuffle
else "none"
508 """Send the media player the command for clear playlist."""
513 self, command: str, parameters: list[str] |
None =
None
515 """Call Squeezebox JSON/RPC method.
517 Additional parameters are added to the command to form the list of
518 positional parameters (p0, p1..., pN) passed to JSON/RPC server.
520 all_params = [command]
522 all_params.extend(parameters)
523 await self.
_player_player.async_query(*all_params)
526 self, command: str, parameters: list[str] |
None =
None
528 """Call Squeezebox JSON/RPC method where we care about the result.
530 Additional parameters are added to the command to form the list of
531 positional parameters (p0, p1..., pN) passed to JSON/RPC server.
533 all_params = [command]
535 all_params.extend(parameters)
537 _LOGGER.debug(
"call_query got result %s", self.
_query_result_query_result)
541 """Add other Squeezebox players to this player's sync group.
543 If the other player is a member of a sync group, it will leave the current sync group
546 ent_reg = er.async_get(self.
hasshasshass)
547 for other_player_entity_id
in group_members:
548 other_player = ent_reg.async_get(other_player_entity_id)
549 if other_player
is None:
551 f
"Could not find player with entity_id {other_player_entity_id}"
553 if other_player_id := other_player.unique_id:
554 await self.
_player_player.async_sync(other_player_id)
557 f
"Could not join unknown player {other_player_entity_id}"
561 """Unsync this Squeezebox player."""
562 await self.
_player_player.async_unsync()
567 media_content_type: MediaType | str |
None =
None,
568 media_content_id: str |
None =
None,
570 """Implement the websocket media browsing helper."""
572 "Reached async_browse_media with content_type %s and content_id %s",
577 if media_content_type
in [
None,
"library"]:
580 if media_content_id
and media_source.is_media_source_id(media_content_id):
581 return await media_source.async_browse_media(
582 self.
hasshasshass, media_content_id, content_filter=media_source_content_filter
586 "search_type": media_content_type,
587 "search_id": media_content_id,
594 media_content_type: MediaType | str,
595 media_content_id: str,
596 media_image_id: str |
None =
None,
597 ) -> tuple[bytes |
None, str |
None]:
598 """Get album art from Squeezebox server."""
600 image_url = self.
_player_player.generate_image_url_from_track_id(media_image_id)
602 if result == (
None,
None):
603 _LOGGER.debug(
"Error retrieving proxied album art from %s", image_url)
None async_write_ha_state(self)
str|UndefinedType|None name(self)
None async_stop(HomeAssistant hass)
None async_discover(DiscoveryInfo discovery_info)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
CALLBACK_TYPE async_at_start(HomeAssistant hass, Callable[[HomeAssistant], Coroutine[Any, Any, None]|None] at_start_cb)