Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """Support for Bluesound devices."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from asyncio import CancelledError, Task
7 from contextlib import suppress
8 from datetime import datetime, timedelta
9 import logging
10 from typing import TYPE_CHECKING, Any
11 
12 from pyblu import Input, Player, Preset, Status, SyncStatus
13 from pyblu.errors import PlayerUnreachableError
14 import voluptuous as vol
15 
16 from homeassistant.components import media_source
18  PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA,
19  BrowseMedia,
20  MediaPlayerEntity,
21  MediaPlayerEntityFeature,
22  MediaPlayerState,
23  MediaType,
24  async_process_play_media_url,
25 )
26 from homeassistant.config_entries import SOURCE_IMPORT
27 from homeassistant.const import CONF_HOST, CONF_HOSTS, CONF_NAME, CONF_PORT
28 from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
29 from homeassistant.data_entry_flow import FlowResultType
30 from homeassistant.exceptions import ServiceValidationError
31 from homeassistant.helpers import config_validation as cv, issue_registry as ir
33  CONNECTION_NETWORK_MAC,
34  DeviceInfo,
35  format_mac,
36 )
37 from homeassistant.helpers.entity_platform import AddEntitiesCallback
38 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
39 import homeassistant.util.dt as dt_util
40 
41 from .const import ATTR_BLUESOUND_GROUP, ATTR_MASTER, DOMAIN, INTEGRATION_TITLE
42 from .utils import format_unique_id
43 
44 if TYPE_CHECKING:
45  from . import BluesoundConfigEntry
46 
47 _LOGGER = logging.getLogger(__name__)
48 
49 SCAN_INTERVAL = timedelta(minutes=15)
50 
51 DATA_BLUESOUND = DOMAIN
52 DEFAULT_PORT = 11000
53 
54 NODE_OFFLINE_CHECK_TIMEOUT = 180
55 NODE_RETRY_INITIATION = timedelta(minutes=3)
56 
57 SYNC_STATUS_INTERVAL = timedelta(minutes=5)
58 
59 POLL_TIMEOUT = 120
60 
61 PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
62  {
63  vol.Optional(CONF_HOSTS): vol.All(
64  cv.ensure_list,
65  [
66  {
67  vol.Required(CONF_HOST): cv.string,
68  vol.Optional(CONF_NAME): cv.string,
69  vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
70  }
71  ],
72  )
73  }
74 )
75 
76 
77 async def _async_import(hass: HomeAssistant, config: ConfigType) -> None:
78  """Import config entry from configuration.yaml."""
79  if not hass.config_entries.async_entries(DOMAIN):
80  # Start import flow
81  result = await hass.config_entries.flow.async_init(
82  DOMAIN, context={"source": SOURCE_IMPORT}, data=config
83  )
84  if (
85  result["type"] == FlowResultType.ABORT
86  and result["reason"] == "cannot_connect"
87  ):
88  ir.async_create_issue(
89  hass,
90  DOMAIN,
91  f"deprecated_yaml_import_issue_{result['reason']}",
92  breaks_in_ha_version="2025.2.0",
93  is_fixable=False,
94  issue_domain=DOMAIN,
95  severity=ir.IssueSeverity.WARNING,
96  translation_key=f"deprecated_yaml_import_issue_{result['reason']}",
97  translation_placeholders={
98  "domain": DOMAIN,
99  "integration_title": INTEGRATION_TITLE,
100  },
101  )
102  return
103 
104  ir.async_create_issue(
105  hass,
106  HOMEASSISTANT_DOMAIN,
107  f"deprecated_yaml_{DOMAIN}",
108  breaks_in_ha_version="2025.2.0",
109  is_fixable=False,
110  issue_domain=DOMAIN,
111  severity=ir.IssueSeverity.WARNING,
112  translation_key="deprecated_yaml",
113  translation_placeholders={
114  "domain": DOMAIN,
115  "integration_title": INTEGRATION_TITLE,
116  },
117  )
118 
119 
121  hass: HomeAssistant,
122  config_entry: BluesoundConfigEntry,
123  async_add_entities: AddEntitiesCallback,
124 ) -> None:
125  """Set up the Bluesound entry."""
126  bluesound_player = BluesoundPlayer(
127  config_entry.data[CONF_HOST],
128  config_entry.data[CONF_PORT],
129  config_entry.runtime_data.player,
130  config_entry.runtime_data.sync_status,
131  )
132 
133  hass.data[DATA_BLUESOUND].append(bluesound_player)
134  async_add_entities([bluesound_player], update_before_add=True)
135 
136 
138  hass: HomeAssistant,
139  config: ConfigType,
140  async_add_entities: AddEntitiesCallback,
141  discovery_info: DiscoveryInfoType | None,
142 ) -> None:
143  """Trigger import flows."""
144  hosts = config.get(CONF_HOSTS, [])
145  for host in hosts:
146  import_data = {
147  CONF_HOST: host[CONF_HOST],
148  CONF_PORT: host.get(CONF_PORT, 11000),
149  }
150  hass.async_create_task(_async_import(hass, import_data))
151 
152 
154  """Representation of a Bluesound Player."""
155 
156  _attr_media_content_type = MediaType.MUSIC
157  _attr_has_entity_name = True
158  _attr_name = None
159 
160  def __init__(
161  self,
162  host: str,
163  port: int,
164  player: Player,
165  sync_status: SyncStatus,
166  ) -> None:
167  """Initialize the media player."""
168  self.hosthost = host
169  self.portport = port
170  self._poll_status_loop_task_poll_status_loop_task: Task[None] | None = None
171  self._poll_sync_status_loop_task_poll_sync_status_loop_task: Task[None] | None = None
172  self._id_id = sync_status.id
173  self._last_status_update_last_status_update: datetime | None = None
174  self._sync_status_sync_status = sync_status
175  self._status_status: Status | None = None
176  self._inputs_inputs: list[Input] = []
177  self._presets_presets: list[Preset] = []
178  self._muted_muted = False
179  self._master_master: BluesoundPlayer | None = None
180  self._is_master_is_master = False
181  self._group_name: str | None = None
182  self._group_list_group_list: list[str] = []
183  self._bluesound_device_name_bluesound_device_name = sync_status.name
184  self._player_player = player
185 
186  self._attr_unique_id_attr_unique_id = format_unique_id(sync_status.mac, port)
187  # there should always be one player with the default port per mac
188  if port == DEFAULT_PORT:
189  self._attr_device_info_attr_device_info = DeviceInfo(
190  identifiers={(DOMAIN, format_mac(sync_status.mac))},
191  connections={(CONNECTION_NETWORK_MAC, format_mac(sync_status.mac))},
192  name=sync_status.name,
193  manufacturer=sync_status.brand,
194  model=sync_status.model_name,
195  model_id=sync_status.model,
196  )
197  else:
198  self._attr_device_info_attr_device_info = DeviceInfo(
199  identifiers={(DOMAIN, format_unique_id(sync_status.mac, port))},
200  name=sync_status.name,
201  manufacturer=sync_status.brand,
202  model=sync_status.model_name,
203  model_id=sync_status.model,
204  via_device=(DOMAIN, format_mac(sync_status.mac)),
205  )
206 
207  async def _poll_status_loop(self) -> None:
208  """Loop which polls the status of the player."""
209  while True:
210  try:
211  await self.async_update_statusasync_update_status()
212  except PlayerUnreachableError:
213  _LOGGER.error(
214  "Node %s:%s is offline, retrying later", self.hosthost, self.portport
215  )
216  await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
217  except CancelledError:
218  _LOGGER.debug(
219  "Stopping the polling of node %s:%s", self.hosthost, self.portport
220  )
221  return
222  except: # noqa: E722 - this loop should never stop
223  _LOGGER.exception(
224  "Unexpected error for %s:%s, retrying later", self.hosthost, self.portport
225  )
226  await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
227 
228  async def _poll_sync_status_loop(self) -> None:
229  """Loop which polls the sync status of the player."""
230  while True:
231  try:
232  await self.update_sync_statusupdate_sync_status()
233  except PlayerUnreachableError:
234  await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
235  except CancelledError:
236  raise
237  except: # noqa: E722 - all errors must be caught for this loop
238  await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
239 
240  async def async_added_to_hass(self) -> None:
241  """Start the polling task."""
242  await super().async_added_to_hass()
243 
244  self._poll_status_loop_task_poll_status_loop_task = self.hasshass.async_create_background_task(
245  self._poll_status_loop_poll_status_loop(),
246  name=f"bluesound.poll_status_loop_{self.host}:{self.port}",
247  )
248  self._poll_sync_status_loop_task_poll_sync_status_loop_task = self.hasshass.async_create_background_task(
249  self._poll_sync_status_loop_poll_sync_status_loop(),
250  name=f"bluesound.poll_sync_status_loop_{self.host}:{self.port}",
251  )
252 
253  async def async_will_remove_from_hass(self) -> None:
254  """Stop the polling task."""
255  await super().async_will_remove_from_hass()
256 
257  assert self._poll_status_loop_task_poll_status_loop_task is not None
258  if self._poll_status_loop_task_poll_status_loop_task.cancel():
259  # the sleeps in _poll_loop will raise CancelledError
260  with suppress(CancelledError):
261  await self._poll_status_loop_task_poll_status_loop_task
262 
263  assert self._poll_sync_status_loop_task_poll_sync_status_loop_task is not None
264  if self._poll_sync_status_loop_task_poll_sync_status_loop_task.cancel():
265  # the sleeps in _poll_sync_status_loop will raise CancelledError
266  with suppress(CancelledError):
267  await self._poll_sync_status_loop_task_poll_sync_status_loop_task
268 
269  self.hasshass.data[DATA_BLUESOUND].remove(self)
270 
271  async def async_update(self) -> None:
272  """Update internal status of the entity."""
273  if not self.availableavailable:
274  return
275 
276  with suppress(PlayerUnreachableError):
277  await self.async_update_presetsasync_update_presets()
278  await self.async_update_capturesasync_update_captures()
279 
280  async def async_update_status(self) -> None:
281  """Use the poll session to always get the status of the player."""
282  etag = None
283  if self._status_status is not None:
284  etag = self._status_status.etag
285 
286  try:
287  status = await self._player_player.status(
288  etag=etag, poll_timeout=POLL_TIMEOUT, timeout=POLL_TIMEOUT + 5
289  )
290 
291  self._attr_available_attr_available = True
292  self._last_status_update_last_status_update = dt_util.utcnow()
293  self._status_status = status
294 
295  self.async_write_ha_stateasync_write_ha_state()
296  except PlayerUnreachableError:
297  self._attr_available_attr_available = False
298  self._last_status_update_last_status_update = None
299  self._status_status = None
300  self.async_write_ha_stateasync_write_ha_state()
301  _LOGGER.error(
302  "Client connection error, marking %s as offline",
303  self._bluesound_device_name_bluesound_device_name,
304  )
305  raise
306 
307  async def update_sync_status(self) -> None:
308  """Update the internal status."""
309  etag = None
310  if self._sync_status_sync_status:
311  etag = self._sync_status_sync_status.etag
312  sync_status = await self._player_player.sync_status(
313  etag=etag, poll_timeout=POLL_TIMEOUT, timeout=POLL_TIMEOUT + 5
314  )
315 
316  self._sync_status_sync_status = sync_status
317 
318  self._group_list_group_list = self.rebuild_bluesound_grouprebuild_bluesound_group()
319 
320  if sync_status.master is not None:
321  self._is_master_is_master = False
322  master_id = f"{sync_status.master.ip}:{sync_status.master.port}"
323  master_device = [
324  device
325  for device in self.hasshass.data[DATA_BLUESOUND]
326  if device.id == master_id
327  ]
328 
329  if master_device and master_id != self.ididid:
330  self._master_master = master_device[0]
331  else:
332  self._master_master = None
333  _LOGGER.error("Master not found %s", master_id)
334  else:
335  if self._master_master is not None:
336  self._master_master = None
337  slaves = self._sync_status_sync_status.slaves
338  self._is_master_is_master = slaves is not None
339 
340  self.async_write_ha_stateasync_write_ha_state()
341 
342  async def async_update_captures(self) -> None:
343  """Update Capture sources."""
344  inputs = await self._player_player.inputs()
345  self._inputs_inputs = inputs
346 
347  async def async_update_presets(self) -> None:
348  """Update Presets."""
349  presets = await self._player_player.presets()
350  self._presets_presets = presets
351 
352  @property
353  def state(self) -> MediaPlayerState:
354  """Return the state of the device."""
355  if self._status_status is None:
356  return MediaPlayerState.OFF
357 
358  if self.is_groupedis_grouped and not self.is_masteris_master:
359  return MediaPlayerState.IDLE
360 
361  match self._status_status.state:
362  case "pause":
363  return MediaPlayerState.PAUSED
364  case "stream" | "play":
365  return MediaPlayerState.PLAYING
366  case _:
367  return MediaPlayerState.IDLE
368 
369  @property
370  def media_title(self) -> str | None:
371  """Title of current playing media."""
372  if self._status_status is None or (self.is_groupedis_grouped and not self.is_masteris_master):
373  return None
374 
375  return self._status_status.name
376 
377  @property
378  def media_artist(self) -> str | None:
379  """Artist of current playing media (Music track only)."""
380  if self._status_status is None:
381  return None
382 
383  if self.is_groupedis_grouped and not self.is_masteris_master:
384  return self._group_name
385 
386  return self._status_status.artist
387 
388  @property
389  def media_album_name(self) -> str | None:
390  """Artist of current playing media (Music track only)."""
391  if self._status_status is None or (self.is_groupedis_grouped and not self.is_masteris_master):
392  return None
393 
394  return self._status_status.album
395 
396  @property
397  def media_image_url(self) -> str | None:
398  """Image url of current playing media."""
399  if self._status_status is None or (self.is_groupedis_grouped and not self.is_masteris_master):
400  return None
401 
402  url = self._status_status.image
403  if url is None:
404  return None
405 
406  if url[0] == "/":
407  url = f"http://{self.host}:{self.port}{url}"
408 
409  return url
410 
411  @property
412  def media_position(self) -> int | None:
413  """Position of current playing media in seconds."""
414  if self._status_status is None or (self.is_groupedis_grouped and not self.is_masteris_master):
415  return None
416 
417  mediastate = self.statestatestatestatestate
418  if self._last_status_update_last_status_update is None or mediastate == MediaPlayerState.IDLE:
419  return None
420 
421  position = self._status_status.seconds
422  if position is None:
423  return None
424 
425  if mediastate == MediaPlayerState.PLAYING:
426  position += (dt_util.utcnow() - self._last_status_update_last_status_update).total_seconds()
427 
428  return int(position)
429 
430  @property
431  def media_duration(self) -> int | None:
432  """Duration of current playing media in seconds."""
433  if self._status_status is None or (self.is_groupedis_grouped and not self.is_masteris_master):
434  return None
435 
436  duration = self._status_status.total_seconds
437  if duration is None:
438  return None
439 
440  return int(duration)
441 
442  @property
443  def media_position_updated_at(self) -> datetime | None:
444  """Last time status was updated."""
445  return self._last_status_update_last_status_update
446 
447  @property
448  def volume_level(self) -> float | None:
449  """Volume level of the media player (0..1)."""
450  volume = None
451 
452  if self._status_status is not None:
453  volume = self._status_status.volume
454  if self.is_groupedis_grouped:
455  volume = self._sync_status_sync_status.volume
456 
457  if volume is None:
458  return None
459 
460  return volume / 100
461 
462  @property
463  def is_volume_muted(self) -> bool:
464  """Boolean if volume is currently muted."""
465  mute = False
466 
467  if self._status_status is not None:
468  mute = self._status_status.mute
469  if self.is_groupedis_grouped:
470  mute = self._sync_status_sync_status.mute_volume is not None
471 
472  return mute
473 
474  @property
475  def id(self) -> str | None:
476  """Get id of device."""
477  return self._id_id
478 
479  @property
480  def bluesound_device_name(self) -> str | None:
481  """Return the device name as returned by the device."""
482  return self._bluesound_device_name_bluesound_device_name
483 
484  @property
485  def sync_status(self) -> SyncStatus:
486  """Return the sync status."""
487  return self._sync_status_sync_status
488 
489  @property
490  def source_list(self) -> list[str] | None:
491  """List of available input sources."""
492  if self._status_status is None or (self.is_groupedis_grouped and not self.is_masteris_master):
493  return None
494 
495  sources = [x.text for x in self._inputs_inputs]
496  sources += [x.name for x in self._presets_presets]
497 
498  return sources
499 
500  @property
501  def source(self) -> str | None:
502  """Name of the current input source."""
503  if self._status_status is None or (self.is_groupedis_grouped and not self.is_masteris_master):
504  return None
505 
506  if self._status_status.input_id is not None:
507  for input_ in self._inputs_inputs:
508  if input_.id == self._status_status.input_id:
509  return input_.text
510 
511  for preset in self._presets_presets:
512  if preset.url == self._status_status.stream_url:
513  return preset.name
514 
515  return self._status_status.service
516 
517  @property
518  def supported_features(self) -> MediaPlayerEntityFeature:
519  """Flag of media commands that are supported."""
520  if self._status_status is None:
521  return MediaPlayerEntityFeature(0)
522 
523  if self.is_groupedis_grouped and not self.is_masteris_master:
524  return (
525  MediaPlayerEntityFeature.VOLUME_STEP
526  | MediaPlayerEntityFeature.VOLUME_SET
527  | MediaPlayerEntityFeature.VOLUME_MUTE
528  )
529 
530  supported = (
531  MediaPlayerEntityFeature.CLEAR_PLAYLIST
532  | MediaPlayerEntityFeature.BROWSE_MEDIA
533  )
534 
535  if not self._status_status.indexing:
536  supported = (
537  supported
538  | MediaPlayerEntityFeature.PAUSE
539  | MediaPlayerEntityFeature.PREVIOUS_TRACK
540  | MediaPlayerEntityFeature.NEXT_TRACK
541  | MediaPlayerEntityFeature.PLAY_MEDIA
542  | MediaPlayerEntityFeature.STOP
543  | MediaPlayerEntityFeature.PLAY
544  | MediaPlayerEntityFeature.SELECT_SOURCE
545  | MediaPlayerEntityFeature.SHUFFLE_SET
546  )
547 
548  current_vol = self.volume_levelvolume_levelvolume_level
549  if current_vol is not None and current_vol >= 0:
550  supported = (
551  supported
552  | MediaPlayerEntityFeature.VOLUME_STEP
553  | MediaPlayerEntityFeature.VOLUME_SET
554  | MediaPlayerEntityFeature.VOLUME_MUTE
555  )
556 
557  if self._status_status.can_seek:
558  supported = supported | MediaPlayerEntityFeature.SEEK
559 
560  return supported
561 
562  @property
563  def is_master(self) -> bool:
564  """Return true if player is a coordinator."""
565  return self._is_master_is_master
566 
567  @property
568  def is_grouped(self) -> bool:
569  """Return true if player is a coordinator."""
570  return self._master_master is not None or self._is_master_is_master
571 
572  @property
573  def shuffle(self) -> bool:
574  """Return true if shuffle is active."""
575  shuffle = False
576  if self._status_status is not None:
577  shuffle = self._status_status.shuffle
578 
579  return shuffle
580 
581  async def async_join(self, master: str) -> None:
582  """Join the player to a group."""
583  master_device = [
584  device
585  for device in self.hasshass.data[DATA_BLUESOUND]
586  if device.entity_id == master
587  ]
588 
589  if len(master_device) > 0:
590  if self.ididid == master_device[0].id:
591  raise ServiceValidationError("Cannot join player to itself")
592 
593  _LOGGER.debug(
594  "Trying to join player: %s to master: %s",
595  self.ididid,
596  master_device[0].id,
597  )
598 
599  await master_device[0].async_add_slave(self)
600  else:
601  _LOGGER.error("Master not found %s", master_device)
602 
603  @property
604  def extra_state_attributes(self) -> dict[str, Any] | None:
605  """List members in group."""
606  attributes: dict[str, Any] = {}
607  if self._group_list_group_list:
608  attributes = {ATTR_BLUESOUND_GROUP: self._group_list_group_list}
609 
610  attributes[ATTR_MASTER] = self._is_master_is_master
611 
612  return attributes
613 
614  def rebuild_bluesound_group(self) -> list[str]:
615  """Rebuild the list of entities in speaker group."""
616  if self.sync_statussync_status.master is None and self.sync_statussync_status.slaves is None:
617  return []
618 
619  player_entities: list[BluesoundPlayer] = self.hasshass.data[DATA_BLUESOUND]
620 
621  leader_sync_status: SyncStatus | None = None
622  if self.sync_statussync_status.master is None:
623  leader_sync_status = self.sync_statussync_status
624  else:
625  required_id = f"{self.sync_status.master.ip}:{self.sync_status.master.port}"
626  for x in player_entities:
627  if x.sync_status.id == required_id:
628  leader_sync_status = x.sync_status
629  break
630 
631  if leader_sync_status is None or leader_sync_status.slaves is None:
632  return []
633 
634  follower_ids = [f"{x.ip}:{x.port}" for x in leader_sync_status.slaves]
635  follower_names = [
636  x.sync_status.name
637  for x in player_entities
638  if x.sync_status.id in follower_ids
639  ]
640  follower_names.insert(0, leader_sync_status.name)
641  return follower_names
642 
643  async def async_unjoin(self) -> None:
644  """Unjoin the player from a group."""
645  if self._master_master is None:
646  return
647 
648  _LOGGER.debug("Trying to unjoin player: %s", self.ididid)
649  await self._master_master.async_remove_slave(self)
650 
651  async def async_add_slave(self, slave_device: BluesoundPlayer) -> None:
652  """Add slave to master."""
653  await self._player_player.add_slave(slave_device.host, slave_device.port)
654 
655  async def async_remove_slave(self, slave_device: BluesoundPlayer) -> None:
656  """Remove slave to master."""
657  await self._player_player.remove_slave(slave_device.host, slave_device.port)
658 
659  async def async_increase_timer(self) -> int:
660  """Increase sleep time on player."""
661  return await self._player_player.sleep_timer()
662 
663  async def async_clear_timer(self) -> None:
664  """Clear sleep timer on player."""
665  sleep = 1
666  while sleep > 0:
667  sleep = await self._player_player.sleep_timer()
668 
669  async def async_set_shuffle(self, shuffle: bool) -> None:
670  """Enable or disable shuffle mode."""
671  await self._player_player.shuffle(shuffle)
672 
673  async def async_select_source(self, source: str) -> None:
674  """Select input source."""
675  if self.is_groupedis_grouped and not self.is_masteris_master:
676  return
677 
678  # presets and inputs might have the same name; presets have priority
679  url: str | None = None
680  for input_ in self._inputs_inputs:
681  if input_.text == source:
682  url = input_.url
683  for preset in self._presets_presets:
684  if preset.name == source:
685  url = preset.url
686 
687  if url is None:
688  raise ServiceValidationError(f"Source {source} not found")
689 
690  await self._player_player.play_url(url)
691 
692  async def async_clear_playlist(self) -> None:
693  """Clear players playlist."""
694  if self.is_groupedis_grouped and not self.is_masteris_master:
695  return
696 
697  await self._player_player.clear()
698 
699  async def async_media_next_track(self) -> None:
700  """Send media_next command to media player."""
701  if self.is_groupedis_grouped and not self.is_masteris_master:
702  return
703 
704  await self._player_player.skip()
705 
706  async def async_media_previous_track(self) -> None:
707  """Send media_previous command to media player."""
708  if self.is_groupedis_grouped and not self.is_masteris_master:
709  return
710 
711  await self._player_player.back()
712 
713  async def async_media_play(self) -> None:
714  """Send media_play command to media player."""
715  if self.is_groupedis_grouped and not self.is_masteris_master:
716  return
717 
718  await self._player_player.play()
719 
720  async def async_media_pause(self) -> None:
721  """Send media_pause command to media player."""
722  if self.is_groupedis_grouped and not self.is_masteris_master:
723  return
724 
725  await self._player_player.pause()
726 
727  async def async_media_stop(self) -> None:
728  """Send stop command."""
729  if self.is_groupedis_grouped and not self.is_masteris_master:
730  return
731 
732  await self._player_player.stop()
733 
734  async def async_media_seek(self, position: float) -> None:
735  """Send media_seek command to media player."""
736  if self.is_groupedis_grouped and not self.is_masteris_master:
737  return
738 
739  await self._player_player.play(seek=int(position))
740 
741  async def async_play_media(
742  self, media_type: MediaType | str, media_id: str, **kwargs: Any
743  ) -> None:
744  """Send the play_media command to the media player."""
745  if self.is_groupedis_grouped and not self.is_masteris_master:
746  return
747 
748  if media_source.is_media_source_id(media_id):
749  play_item = await media_source.async_resolve_media(
750  self.hasshass, media_id, self.entity_identity_id
751  )
752  media_id = play_item.url
753 
754  url = async_process_play_media_url(self.hasshass, media_id)
755 
756  await self._player_player.play_url(url)
757 
758  async def async_volume_up(self) -> None:
759  """Volume up the media player."""
760  if self.volume_levelvolume_levelvolume_level is None:
761  return
762 
763  new_volume = self.volume_levelvolume_levelvolume_level + 0.01
764  new_volume = min(1, new_volume)
765  await self.async_set_volume_levelasync_set_volume_levelasync_set_volume_level(new_volume)
766 
767  async def async_volume_down(self) -> None:
768  """Volume down the media player."""
769  if self.volume_levelvolume_levelvolume_level is None:
770  return
771 
772  new_volume = self.volume_levelvolume_levelvolume_level - 0.01
773  new_volume = max(0, new_volume)
774  await self.async_set_volume_levelasync_set_volume_levelasync_set_volume_level(new_volume)
775 
776  async def async_set_volume_level(self, volume: float) -> None:
777  """Send volume_up command to media player."""
778  volume = int(round(volume * 100))
779  volume = min(100, volume)
780  volume = max(0, volume)
781 
782  await self._player_player.volume(level=volume)
783 
784  async def async_mute_volume(self, mute: bool) -> None:
785  """Send mute command to media player."""
786  await self._player_player.volume(mute=mute)
787 
789  self,
790  media_content_type: MediaType | str | None = None,
791  media_content_id: str | None = None,
792  ) -> BrowseMedia:
793  """Implement the websocket media browsing helper."""
794  return await media_source.async_browse_media(
795  self.hasshass,
796  media_content_id,
797  content_filter=lambda item: item.media_content_type.startswith("audio/"),
798  )
BrowseMedia async_browse_media(self, MediaType|str|None media_content_type=None, str|None media_content_id=None)
None async_add_slave(self, BluesoundPlayer slave_device)
None __init__(self, str host, int port, Player player, SyncStatus sync_status)
None async_play_media(self, MediaType|str media_type, str media_id, **Any kwargs)
None async_remove_slave(self, BluesoundPlayer slave_device)
None async_setup_entry(HomeAssistant hass, BluesoundConfigEntry config_entry, AddEntitiesCallback async_add_entities)
None _async_import(HomeAssistant hass, ConfigType config)
Definition: media_player.py:77
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info)
str format_unique_id(str mac, int port)
Definition: utils.py:6
bool remove(self, _T matcher)
Definition: match.py:214
str async_process_play_media_url(HomeAssistant hass, str media_content_id, *bool allow_relative_url=False, bool for_supervisor_network=False)
Definition: browse_media.py:36