Esempio n. 1
0
    def play_media(self, media_type, media_id, **kwargs):
        # Play a piece of media.
        # <ServiceCall media_player.play_media: media_content_type=music, media_content_id=http://192.168.13.91:8123/api/tts_proxy/74a4297365735b6c107b85e034347ce013eeae01_en_-_google.mp3, entity_id=['media_player.mt_office']>
        self._parent.send('mrad.setzone "{}"'.format(self._zoneId))
        self._parent.send('mrad.setsource')

        media_type = media_type.lower()

        if media_source.is_media_source_id(media_id):
            media_type = "music"
            media_id = (asyncio.run_coroutine_threadsafe(
                media_source.async_resolve_media(self._hass, media_id,
                                                 self.entity_id),
                self._hass.loop,
            ).result().url)
            media_id = async_process_play_media_url(self.hass, media_id)

        if media_type == "music":
            self._parent.send('duckplay "{}"'.format(media_id))
        elif media_type == "scene":
            self._parent.send('recallscene "{}"'.format(media_id))
        elif media_type == "preset":
            self._parent.send('recallpreset "{}"'.format(media_id))
        elif media_type == "radiostation":
            self._parent.send('playradiostation "{}"'.format(media_id))
        else:
            _LOGGER.error("Unexpected media_type='%s'.", media_type)
    async def async_play_media(self, media_type: str, media_id: str,
                               **kwargs: Any) -> None:
        """Play media from a URL or file."""
        # Handle media_source
        if media_source.is_media_source_id(media_id):
            sourced_media = await media_source.async_resolve_media(
                self.hass, media_id)
            media_type = sourced_media.mime_type
            media_id = sourced_media.url

        if media_type != MEDIA_TYPE_MUSIC and not media_type.startswith(
                "audio/"):
            raise HomeAssistantError(
                f"Invalid media type {media_type}. Only {MEDIA_TYPE_MUSIC} is supported"
            )

        # If media ID is a relative URL, we serve it from HA.
        media_id = async_process_play_media_url(
            self.hass, media_id, for_supervisor_network=self._using_addon)

        await self._vlc.add(media_id)
        self._state = STATE_PLAYING
Esempio n. 3
0
    async def async_play_media(self, media_type: str, media_id: str,
                               **kwargs: Any) -> None:
        """Play media from a URL or file, launch an application, or tune to a channel."""
        extra: dict[str, Any] = kwargs.get(ATTR_MEDIA_EXTRA) or {}
        original_media_type: str = media_type
        original_media_id: str = media_id
        mime_type: str | None = None
        stream_name: str | None = None
        stream_format: str | None = extra.get(ATTR_FORMAT)

        # Handle media_source
        if media_source.is_media_source_id(media_id):
            sourced_media = await media_source.async_resolve_media(
                self.hass, media_id)
            media_type = MEDIA_TYPE_URL
            media_id = sourced_media.url
            mime_type = sourced_media.mime_type
            stream_name = original_media_id
            stream_format = guess_stream_format(media_id, mime_type)

        # If media ID is a relative URL, we serve it from HA.
        media_id = async_process_play_media_url(self.hass, media_id)

        if media_type == FORMAT_CONTENT_TYPE[HLS_PROVIDER]:
            media_type = MEDIA_TYPE_VIDEO
            mime_type = FORMAT_CONTENT_TYPE[HLS_PROVIDER]
            stream_name = "Camera Stream"
            stream_format = "hls"

        if media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_URL, MEDIA_TYPE_VIDEO):
            parsed = yarl.URL(media_id)

            if mime_type is None:
                mime_type, _ = mimetypes.guess_type(parsed.path)

            if stream_format is None:
                stream_format = guess_stream_format(media_id, mime_type)

            if extra.get(ATTR_FORMAT) is None:
                extra[ATTR_FORMAT] = stream_format

            if extra[ATTR_FORMAT] not in STREAM_FORMAT_TO_MEDIA_TYPE:
                _LOGGER.error(
                    "Media type %s is not supported with format %s (mime: %s)",
                    original_media_type,
                    extra[ATTR_FORMAT],
                    mime_type,
                )
                return

            if (media_type == MEDIA_TYPE_URL
                    and STREAM_FORMAT_TO_MEDIA_TYPE[extra[ATTR_FORMAT]]
                    == MEDIA_TYPE_MUSIC):
                media_type = MEDIA_TYPE_MUSIC

            if media_type == MEDIA_TYPE_MUSIC and "tts_proxy" in media_id:
                stream_name = "Text to Speech"
            elif stream_name is None:
                if stream_format == "ism":
                    stream_name = parsed.parts[-2]
                else:
                    stream_name = parsed.name

            if extra.get(ATTR_NAME) is None:
                extra[ATTR_NAME] = stream_name

        if media_type == MEDIA_TYPE_APP:
            params = {
                param: extra[attr]
                for attr, param in ATTRS_TO_LAUNCH_PARAMS.items()
                if attr in extra
            }

            await self.coordinator.roku.launch(media_id, params)
        elif media_type == MEDIA_TYPE_CHANNEL:
            await self.coordinator.roku.tune(media_id)
        elif media_type == MEDIA_TYPE_MUSIC:
            if extra.get(ATTR_ARTIST_NAME) is None:
                extra[ATTR_ARTIST_NAME] = "Home Assistant"

            params = {
                param: extra[attr]
                for (attr,
                     param) in ATTRS_TO_PLAY_ON_ROKU_AUDIO_PARAMS.items()
                if attr in extra
            }

            params = {"t": "a", **params}

            await self.coordinator.roku.play_on_roku(media_id, params)
        elif media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_VIDEO):
            params = {
                param: extra[attr]
                for (attr, param) in ATTRS_TO_PLAY_ON_ROKU_PARAMS.items()
                if attr in extra
            }

            await self.coordinator.roku.play_on_roku(media_id, params)
        else:
            _LOGGER.error("Media type %s is not supported",
                          original_media_type)
            return

        await self.coordinator.async_request_refresh()
Esempio n. 4
0
    def play_media(  # noqa: C901
            self, media_type: str, media_id: str, **kwargs: Any) -> None:
        """
        Send the play_media command to the media player.

        If media_id is a Plex payload, attempt Plex->Sonos playback.

        If media_id is an Apple Music, Deezer, Sonos, or Tidal share link,
        attempt playback using the respective service.

        If media_type is "playlist", media_id should be a Sonos
        Playlist name.  Otherwise, media_id should be a URI.
        """
        # Use 'replace' as the default enqueue option
        enqueue = kwargs.get(ATTR_MEDIA_ENQUEUE, MediaPlayerEnqueue.REPLACE)

        if spotify.is_spotify_media_type(media_type):
            media_type = spotify.resolve_spotify_media_type(media_type)
            media_id = spotify.spotify_uri_from_media_browser_url(media_id)

        is_radio = False

        if media_source.is_media_source_id(media_id):
            is_radio = media_id.startswith("media-source://radio_browser/")
            media_type = MEDIA_TYPE_MUSIC
            media_id = (run_coroutine_threadsafe(
                media_source.async_resolve_media(self.hass, media_id,
                                                 self.entity_id),
                self.hass.loop,
            ).result().url)

        if media_type == "favorite_item_id":
            favorite = self.speaker.favorites.lookup_by_item_id(media_id)
            if favorite is None:
                raise ValueError(f"Missing favorite for media_id: {media_id}")
            self._play_favorite(favorite)
            return

        soco = self.coordinator.soco
        if media_id and media_id.startswith(PLEX_URI_SCHEME):
            plex_plugin = self.speaker.plex_plugin
            result = process_plex_payload(self.hass,
                                          media_type,
                                          media_id,
                                          supports_playqueues=False)
            if result.shuffle:
                self.set_shuffle(True)
            if enqueue == MediaPlayerEnqueue.ADD:
                plex_plugin.add_to_queue(result.media)
            elif enqueue in (
                    MediaPlayerEnqueue.NEXT,
                    MediaPlayerEnqueue.PLAY,
            ):
                pos = (self.media.queue_position or 0) + 1
                new_pos = plex_plugin.add_to_queue(result.media, position=pos)
                if enqueue == MediaPlayerEnqueue.PLAY:
                    soco.play_from_queue(new_pos - 1)
            elif enqueue == MediaPlayerEnqueue.REPLACE:
                soco.clear_queue()
                plex_plugin.add_to_queue(result.media)
                soco.play_from_queue(0)
            return

        share_link = self.coordinator.share_link
        if share_link.is_share_link(media_id):
            if enqueue == MediaPlayerEnqueue.ADD:
                share_link.add_share_link_to_queue(media_id)
            elif enqueue in (
                    MediaPlayerEnqueue.NEXT,
                    MediaPlayerEnqueue.PLAY,
            ):
                pos = (self.media.queue_position or 0) + 1
                new_pos = share_link.add_share_link_to_queue(media_id,
                                                             position=pos)
                if enqueue == MediaPlayerEnqueue.PLAY:
                    soco.play_from_queue(new_pos - 1)
            elif enqueue == MediaPlayerEnqueue.REPLACE:
                soco.clear_queue()
                share_link.add_share_link_to_queue(media_id)
                soco.play_from_queue(0)
        elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK):
            # If media ID is a relative URL, we serve it from HA.
            media_id = async_process_play_media_url(self.hass, media_id)

            if enqueue == MediaPlayerEnqueue.ADD:
                soco.add_uri_to_queue(media_id)
            elif enqueue in (
                    MediaPlayerEnqueue.NEXT,
                    MediaPlayerEnqueue.PLAY,
            ):
                pos = (self.media.queue_position or 0) + 1
                new_pos = soco.add_uri_to_queue(media_id, position=pos)
                if enqueue == MediaPlayerEnqueue.PLAY:
                    soco.play_from_queue(new_pos - 1)
            elif enqueue == MediaPlayerEnqueue.REPLACE:
                soco.play_uri(media_id, force_radio=is_radio)
        elif media_type == MEDIA_TYPE_PLAYLIST:
            if media_id.startswith("S:"):
                item = media_browser.get_media(
                    self.media.library, media_id,
                    media_type)  # type: ignore[no-untyped-call]
                soco.play_uri(item.get_uri())
                return
            try:
                playlists = soco.get_sonos_playlists()
                playlist = next(p for p in playlists if p.title == media_id)
            except StopIteration:
                _LOGGER.error('Could not find a Sonos playlist named "%s"',
                              media_id)
            else:
                soco.clear_queue()
                soco.add_to_queue(playlist)
                soco.play_from_queue(0)
        elif media_type in PLAYABLE_MEDIA_TYPES:
            item = media_browser.get_media(
                self.media.library, media_id,
                media_type)  # type: ignore[no-untyped-call]

            if not item:
                _LOGGER.error('Could not find "%s" in the library', media_id)
                return

            soco.play_uri(item.get_uri())
        else:
            _LOGGER.error('Sonos does not support a media type of "%s"',
                          media_type)
Esempio n. 5
0
class DlnaDmrEntity(MediaPlayerEntity):
    """Representation of a DLNA DMR device as a HA entity."""

    udn: str
    device_type: str

    _event_addr: EventListenAddr
    poll_availability: bool
    # Last known URL for the device, used when adding this entity to hass to try
    # to connect before SSDP has rediscovered it, or when SSDP discovery fails.
    location: str

    _device_lock: asyncio.Lock  # Held when connecting or disconnecting the device
    _device: DmrDevice | None = None
    check_available: bool = False
    _ssdp_connect_failed: bool = False

    # Track BOOTID in SSDP advertisements for device changes
    _bootid: int | None = None

    # DMR devices need polling for track position information. async_update will
    # determine whether further device polling is required.
    _attr_should_poll = True

    def __init__(
        self,
        udn: str,
        device_type: str,
        name: str,
        event_port: int,
        event_callback_url: str | None,
        poll_availability: bool,
        location: str,
    ) -> None:
        """Initialize DLNA DMR entity."""
        self.udn = udn
        self.device_type = device_type
        self._attr_name = name
        self._event_addr = EventListenAddr(None, event_port,
                                           event_callback_url)
        self.poll_availability = poll_availability
        self.location = location
        self._device_lock = asyncio.Lock()

    async def async_added_to_hass(self) -> None:
        """Handle addition."""
        # Update this entity when the associated config entry is modified
        if self.registry_entry and self.registry_entry.config_entry_id:
            config_entry = self.hass.config_entries.async_get_entry(
                self.registry_entry.config_entry_id)
            assert config_entry is not None
            self.async_on_remove(
                config_entry.add_update_listener(
                    self.async_config_update_listener))

        # Try to connect to the last known location, but don't worry if not available
        if not self._device:
            try:
                await self._device_connect(self.location)
            except UpnpError as err:
                _LOGGER.debug("Couldn't connect immediately: %r", err)

        # Get SSDP notifications for only this device
        self.async_on_remove(await ssdp.async_register_callback(
            self.hass, self.async_ssdp_callback, {"USN": self.usn}))

        # async_upnp_client.SsdpListener only reports byebye once for each *UDN*
        # (device name) which often is not the USN (service within the device)
        # that we're interested in. So also listen for byebye advertisements for
        # the UDN, which is reported in the _udn field of the combined_headers.
        self.async_on_remove(await ssdp.async_register_callback(
            self.hass,
            self.async_ssdp_callback,
            {
                "_udn": self.udn,
                "NTS": NotificationSubType.SSDP_BYEBYE
            },
        ))

    async def async_will_remove_from_hass(self) -> None:
        """Handle removal."""
        await self._device_disconnect()

    async def async_ssdp_callback(self, info: ssdp.SsdpServiceInfo,
                                  change: ssdp.SsdpChange) -> None:
        """Handle notification from SSDP of device state change."""
        _LOGGER.debug(
            "SSDP %s notification of device %s at %s",
            change,
            info.ssdp_usn,
            info.ssdp_location,
        )

        try:
            bootid_str = info.ssdp_headers[ssdp.ATTR_SSDP_BOOTID]
            bootid: int | None = int(bootid_str, 10)
        except (KeyError, ValueError):
            bootid = None

        if change == ssdp.SsdpChange.UPDATE:
            # This is an announcement that bootid is about to change
            if self._bootid is not None and self._bootid == bootid:
                # Store the new value (because our old value matches) so that we
                # can ignore subsequent ssdp:alive messages
                with contextlib.suppress(KeyError, ValueError):
                    next_bootid_str = info.ssdp_headers[
                        ssdp.ATTR_SSDP_NEXTBOOTID]
                    self._bootid = int(next_bootid_str, 10)
            # Nothing left to do until ssdp:alive comes through
            return

        if self._bootid is not None and self._bootid != bootid:
            # Device has rebooted
            # Maybe connection will succeed now
            self._ssdp_connect_failed = False
            if self._device:
                # Drop existing connection and maybe reconnect
                await self._device_disconnect()
        self._bootid = bootid

        if change == ssdp.SsdpChange.BYEBYE:
            # Device is going away
            if self._device:
                # Disconnect from gone device
                await self._device_disconnect()
            # Maybe the next alive message will result in a successful connection
            self._ssdp_connect_failed = False

        if (change == ssdp.SsdpChange.ALIVE and not self._device
                and not self._ssdp_connect_failed):
            assert info.ssdp_location
            location = info.ssdp_location
            try:
                await self._device_connect(location)
            except UpnpError as err:
                self._ssdp_connect_failed = True
                _LOGGER.warning(
                    "Failed connecting to recently alive device at %s: %r",
                    location,
                    err,
                )

        # Device could have been de/re-connected, state probably changed
        self.async_write_ha_state()

    async def async_config_update_listener(
            self, hass: HomeAssistant,
            entry: config_entries.ConfigEntry) -> None:
        """Handle options update by modifying self in-place."""
        _LOGGER.debug(
            "Updating: %s with data=%s and options=%s",
            self.name,
            entry.data,
            entry.options,
        )
        self.location = entry.data[CONF_URL]
        self.poll_availability = entry.options.get(CONF_POLL_AVAILABILITY,
                                                   False)

        new_port = entry.options.get(CONF_LISTEN_PORT) or 0
        new_callback_url = entry.options.get(CONF_CALLBACK_URL_OVERRIDE)

        if (new_port == self._event_addr.port
                and new_callback_url == self._event_addr.callback_url):
            return

        # Changes to eventing requires a device reconnect for it to update correctly
        await self._device_disconnect()
        # Update _event_addr after disconnecting, to stop the right event listener
        self._event_addr = self._event_addr._replace(
            port=new_port, callback_url=new_callback_url)
        try:
            await self._device_connect(self.location)
        except UpnpError as err:
            _LOGGER.warning("Couldn't (re)connect after config change: %r",
                            err)

        # Device was de/re-connected, state might have changed
        self.async_write_ha_state()

    async def _device_connect(self, location: str) -> None:
        """Connect to the device now that it's available."""
        _LOGGER.debug("Connecting to device at %s", location)

        async with self._device_lock:
            if self._device:
                _LOGGER.debug(
                    "Trying to connect when device already connected")
                return

            domain_data = get_domain_data(self.hass)

            # Connect to the base UPNP device
            upnp_device = await domain_data.upnp_factory.async_create_device(
                location)

            # Create/get event handler that is reachable by the device, using
            # the connection's local IP to listen only on the relevant interface
            _, event_ip = await async_get_local_ip(location, self.hass.loop)
            self._event_addr = self._event_addr._replace(host=event_ip)
            event_handler = await domain_data.async_get_event_notifier(
                self._event_addr, self.hass)

            # Create profile wrapper
            self._device = DmrDevice(upnp_device, event_handler)

            self.location = location

            # Subscribe to event notifications
            try:
                self._device.on_event = self._on_event
                await self._device.async_subscribe_services(
                    auto_resubscribe=True)
            except UpnpResponseError as err:
                # Device rejected subscription request. This is OK, variables
                # will be polled instead.
                _LOGGER.debug("Device rejected subscription: %r", err)
            except UpnpError as err:
                # Don't leave the device half-constructed
                self._device.on_event = None
                self._device = None
                await domain_data.async_release_event_notifier(self._event_addr
                                                               )
                _LOGGER.debug(
                    "Error while subscribing during device connect: %r", err)
                raise

        if (not self.registry_entry or not self.registry_entry.config_entry_id
                or self.registry_entry.device_id):
            return

        # Create linked HA DeviceEntry now the information is known.
        dev_reg = device_registry.async_get(self.hass)
        device_entry = dev_reg.async_get_or_create(
            config_entry_id=self.registry_entry.config_entry_id,
            # Connections are based on the root device's UDN, and the DMR
            # embedded device's UDN. They may be the same, if the DMR is the
            # root device.
            connections={
                (
                    device_registry.CONNECTION_UPNP,
                    self._device.profile_device.root_device.udn,
                ),
                (device_registry.CONNECTION_UPNP, self._device.udn),
            },
            identifiers={(DOMAIN, self.unique_id)},
            default_manufacturer=self._device.manufacturer,
            default_model=self._device.model_name,
            default_name=self._device.name,
        )

        # Update entity registry to link to the device
        ent_reg = entity_registry.async_get(self.hass)
        ent_reg.async_get_or_create(
            self.registry_entry.domain,
            self.registry_entry.platform,
            self.unique_id,
            device_id=device_entry.id,
        )

    async def _device_disconnect(self) -> None:
        """Destroy connections to the device now that it's not available.

        Also call when removing this entity from hass to clean up connections.
        """
        async with self._device_lock:
            if not self._device:
                _LOGGER.debug("Disconnecting from device that's not connected")
                return

            _LOGGER.debug("Disconnecting from %s", self._device.name)

            self._device.on_event = None
            old_device = self._device
            self._device = None
            await old_device.async_unsubscribe_services()

        domain_data = get_domain_data(self.hass)
        await domain_data.async_release_event_notifier(self._event_addr)

    async def async_update(self) -> None:
        """Retrieve the latest data."""
        if not self._device:
            if not self.poll_availability:
                return
            try:
                await self._device_connect(self.location)
            except UpnpError:
                return

        assert self._device is not None

        try:
            do_ping = self.poll_availability or self.check_available
            await self._device.async_update(do_ping=do_ping)
        except UpnpError as err:
            _LOGGER.debug("Device unavailable: %r", err)
            await self._device_disconnect()
            return
        finally:
            self.check_available = False

    def _on_event(self, service: UpnpService,
                  state_variables: Sequence[UpnpStateVariable]) -> None:
        """State variable(s) changed, let home-assistant know."""
        if not state_variables:
            # Indicates a failure to resubscribe, check if device is still available
            self.check_available = True

        force_refresh = False

        if service.service_id == "urn:upnp-org:serviceId:AVTransport":
            for state_variable in state_variables:
                # Force a state refresh when player begins or pauses playback
                # to update the position info.
                if (state_variable.name == "TransportState"
                        and state_variable.value
                        in (TransportState.PLAYING,
                            TransportState.PAUSED_PLAYBACK)):
                    force_refresh = True

        self.async_schedule_update_ha_state(force_refresh)

    @property
    def available(self) -> bool:
        """Device is available when we have a connection to it."""
        return self._device is not None and self._device.profile_device.available

    @property
    def unique_id(self) -> str:
        """Report the UDN (Unique Device Name) as this entity's unique ID."""
        return self.udn

    @property
    def usn(self) -> str:
        """Get the USN based on the UDN (Unique Device Name) and device type."""
        return f"{self.udn}::{self.device_type}"

    @property
    def state(self) -> str | None:
        """State of the player."""
        if not self._device or not self.available:
            return STATE_OFF
        if self._device.transport_state is None:
            return STATE_ON
        if self._device.transport_state in (
                TransportState.PLAYING,
                TransportState.TRANSITIONING,
        ):
            return STATE_PLAYING
        if self._device.transport_state in (
                TransportState.PAUSED_PLAYBACK,
                TransportState.PAUSED_RECORDING,
        ):
            return STATE_PAUSED
        if self._device.transport_state == TransportState.VENDOR_DEFINED:
            # Unable to map this state to anything reasonable, so it's "Unknown"
            return None

        return STATE_IDLE

    @property
    def supported_features(self) -> int:
        """Flag media player features that are supported at this moment.

        Supported features may change as the device enters different states.
        """
        if not self._device:
            return 0

        supported_features = 0

        if self._device.has_volume_level:
            supported_features |= MediaPlayerEntityFeature.VOLUME_SET
        if self._device.has_volume_mute:
            supported_features |= MediaPlayerEntityFeature.VOLUME_MUTE
        if self._device.can_play:
            supported_features |= MediaPlayerEntityFeature.PLAY
        if self._device.can_pause:
            supported_features |= MediaPlayerEntityFeature.PAUSE
        if self._device.can_stop:
            supported_features |= MediaPlayerEntityFeature.STOP
        if self._device.can_previous:
            supported_features |= MediaPlayerEntityFeature.PREVIOUS_TRACK
        if self._device.can_next:
            supported_features |= MediaPlayerEntityFeature.NEXT_TRACK
        if self._device.has_play_media:
            supported_features |= (MediaPlayerEntityFeature.PLAY_MEDIA
                                   | MediaPlayerEntityFeature.BROWSE_MEDIA)
        if self._device.can_seek_rel_time:
            supported_features |= MediaPlayerEntityFeature.SEEK

        play_modes = self._device.valid_play_modes
        if play_modes & {PlayMode.RANDOM, PlayMode.SHUFFLE}:
            supported_features |= MediaPlayerEntityFeature.SHUFFLE_SET
        if play_modes & {PlayMode.REPEAT_ONE, PlayMode.REPEAT_ALL}:
            supported_features |= MediaPlayerEntityFeature.REPEAT_SET

        if self._device.has_presets:
            supported_features |= MediaPlayerEntityFeature.SELECT_SOUND_MODE

        return supported_features

    @property
    def volume_level(self) -> float | None:
        """Volume level of the media player (0..1)."""
        if not self._device or not self._device.has_volume_level:
            return None
        return self._device.volume_level

    @catch_request_errors
    async def async_set_volume_level(self, volume: float) -> None:
        """Set volume level, range 0..1."""
        assert self._device is not None
        await self._device.async_set_volume_level(volume)

    @property
    def is_volume_muted(self) -> bool | None:
        """Boolean if volume is currently muted."""
        if not self._device:
            return None
        return self._device.is_volume_muted

    @catch_request_errors
    async def async_mute_volume(self, mute: bool) -> None:
        """Mute the volume."""
        assert self._device is not None
        desired_mute = bool(mute)
        await self._device.async_mute_volume(desired_mute)

    @catch_request_errors
    async def async_media_pause(self) -> None:
        """Send pause command."""
        assert self._device is not None
        await self._device.async_pause()

    @catch_request_errors
    async def async_media_play(self) -> None:
        """Send play command."""
        assert self._device is not None
        await self._device.async_play()

    @catch_request_errors
    async def async_media_stop(self) -> None:
        """Send stop command."""
        assert self._device is not None
        await self._device.async_stop()

    @catch_request_errors
    async def async_media_seek(self, position: int | float) -> None:
        """Send seek command."""
        assert self._device is not None
        time = timedelta(seconds=position)
        await self._device.async_seek_rel_time(time)

    @catch_request_errors
    async def async_play_media(self, media_type: str, media_id: str,
                               **kwargs: Any) -> None:
        """Play a piece of media."""
        _LOGGER.debug("Playing media: %s, %s, %s", media_type, media_id,
                      kwargs)
        assert self._device is not None

        didl_metadata: str | None = None
        title: str = ""

        # If media is media_source, resolve it to url and MIME type, and maybe metadata
        if media_source.is_media_source_id(media_id):
            sourced_media = await media_source.async_resolve_media(
                self.hass, media_id)
            media_type = sourced_media.mime_type
            media_id = sourced_media.url
            _LOGGER.debug("sourced_media is %s", sourced_media)
            if sourced_metadata := getattr(sourced_media, "didl_metadata",
                                           None):
                didl_metadata = didl_lite.to_xml_string(
                    sourced_metadata).decode("utf-8")
                title = sourced_metadata.title

        # If media ID is a relative URL, we serve it from HA.
        media_id = async_process_play_media_url(self.hass, media_id)

        extra: dict[str, Any] = kwargs.get(ATTR_MEDIA_EXTRA) or {}
        metadata: dict[str, Any] = extra.get("metadata") or {}

        if not title:
            title = extra.get("title") or metadata.get(
                "title") or "Home Assistant"
        if thumb := extra.get("thumb"):
            metadata["album_art_uri"] = thumb
Esempio n. 6
0
    async def async_play_media(self, media_type, media_id, **kwargs):
        """Play a piece of media."""
        # Handle media_source
        if media_source.is_media_source_id(media_id):
            sourced_media = await media_source.async_resolve_media(
                self.hass, media_id)
            media_type = sourced_media.mime_type
            media_id = sourced_media.url

        extra = kwargs.get(ATTR_MEDIA_EXTRA, {})
        metadata = extra.get("metadata")

        # Handle media supported by a known cast app
        if media_type == CAST_DOMAIN:
            try:
                app_data = json.loads(media_id)
                if metadata is not None:
                    app_data["metadata"] = extra.get("metadata")
            except json.JSONDecodeError:
                _LOGGER.error("Invalid JSON in media_content_id")
                raise

            # Special handling for passed `app_id` parameter. This will only launch
            # an arbitrary cast app, generally for UX.
            if "app_id" in app_data:
                app_id = app_data.pop("app_id")
                _LOGGER.info("Starting Cast app by ID %s", app_id)
                await self.hass.async_add_executor_job(
                    self._chromecast.start_app, app_id)
                if app_data:
                    _LOGGER.warning(
                        "Extra keys %s were ignored. Please use app_name to cast media",
                        app_data.keys(),
                    )
                return

            app_name = app_data.pop("app_name")
            try:
                await self.hass.async_add_executor_job(quick_play,
                                                       self._chromecast,
                                                       app_name, app_data)
            except NotImplementedError:
                _LOGGER.error("App %s not supported", app_name)
            return

        # Try the cast platforms
        for platform in self.hass.data[CAST_DOMAIN].values():
            result = await platform.async_play_media(self.hass, self.entity_id,
                                                     self._chromecast,
                                                     media_type, media_id)
            if result:
                return

        # If media ID is a relative URL, we serve it from HA.
        media_id = async_process_play_media_url(self.hass, media_id)

        # Configure play command for when playing a HLS stream
        if is_hass_url(self.hass, media_id):
            parsed = yarl.URL(media_id)
            if parsed.path.startswith("/api/hls/"):
                extra = {
                    **extra,
                    "stream_type": "LIVE",
                    "media_info": {
                        "hlsVideoSegmentFormat": "fmp4",
                    },
                }

        # Default to play with the default media receiver
        app_data = {"media_id": media_id, "media_type": media_type, **extra}
        await self.hass.async_add_executor_job(quick_play, self._chromecast,
                                               "default_media_receiver",
                                               app_data)
Esempio n. 7
0
    def play_media(self, media_type: str, media_id: str,
                   **kwargs: Any) -> None:
        """
        Send the play_media command to the media player.

        If media_id is a Plex payload, attempt Plex->Sonos playback.

        If media_id is an Apple Music, Deezer, Sonos, or Tidal share link,
        attempt playback using the respective service.

        If media_type is "playlist", media_id should be a Sonos
        Playlist name.  Otherwise, media_id should be a URI.

        If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue.
        """
        if spotify.is_spotify_media_type(media_type):
            media_type = spotify.resolve_spotify_media_type(media_type)
            media_id = spotify.spotify_uri_from_media_browser_url(media_id)

        is_radio = False

        if media_source.is_media_source_id(media_id):
            is_radio = media_id.startswith("media-source://radio_browser/")
            media_type = MEDIA_TYPE_MUSIC
            media_id = (run_coroutine_threadsafe(
                media_source.async_resolve_media(self.hass, media_id),
                self.hass.loop,
            ).result().url)

        if media_type == "favorite_item_id":
            favorite = self.speaker.favorites.lookup_by_item_id(media_id)
            if favorite is None:
                raise ValueError(f"Missing favorite for media_id: {media_id}")
            self._play_favorite(favorite)
            return

        soco = self.coordinator.soco
        if media_id and media_id.startswith(PLEX_URI_SCHEME):
            plex_plugin = self.speaker.plex_plugin
            media_id = media_id[len(PLEX_URI_SCHEME):]
            payload = json.loads(media_id)
            if isinstance(payload, dict):
                shuffle = payload.pop("shuffle", False)
            else:
                shuffle = False
            media = lookup_plex_media(self.hass, media_type,
                                      json.dumps(payload))
            if not kwargs.get(ATTR_MEDIA_ENQUEUE):
                soco.clear_queue()
            if shuffle:
                self.set_shuffle(True)
            plex_plugin.play_now(media)
            return

        share_link = self.coordinator.share_link
        if share_link.is_share_link(media_id):
            if kwargs.get(ATTR_MEDIA_ENQUEUE):
                share_link.add_share_link_to_queue(media_id)
            else:
                soco.clear_queue()
                share_link.add_share_link_to_queue(media_id)
                soco.play_from_queue(0)
        elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK):
            # If media ID is a relative URL, we serve it from HA.
            media_id = async_process_play_media_url(self.hass, media_id)

            if kwargs.get(ATTR_MEDIA_ENQUEUE):
                soco.add_uri_to_queue(media_id)
            else:
                soco.play_uri(media_id, force_radio=is_radio)
        elif media_type == MEDIA_TYPE_PLAYLIST:
            if media_id.startswith("S:"):
                item = media_browser.get_media(
                    self.media.library, media_id,
                    media_type)  # type: ignore[no-untyped-call]
                soco.play_uri(item.get_uri())
                return
            try:
                playlists = soco.get_sonos_playlists()
                playlist = next(p for p in playlists if p.title == media_id)
            except StopIteration:
                _LOGGER.error('Could not find a Sonos playlist named "%s"',
                              media_id)
            else:
                soco.clear_queue()
                soco.add_to_queue(playlist)
                soco.play_from_queue(0)
        elif media_type in PLAYABLE_MEDIA_TYPES:
            item = media_browser.get_media(
                self.media.library, media_id,
                media_type)  # type: ignore[no-untyped-call]

            if not item:
                _LOGGER.error('Could not find "%s" in the library', media_id)
                return

            soco.play_uri(item.get_uri())
        else:
            _LOGGER.error('Sonos does not support a media type of "%s"',
                          media_type)
Esempio n. 8
0
    async def async_play_media(
        self, media_type: str, media_id: str, **kwargs: Any
    ) -> None:
        """Play a piece of media."""
        chromecast = self._get_chromecast()
        # Handle media_source
        if media_source.is_media_source_id(media_id):
            sourced_media = await media_source.async_resolve_media(
                self.hass, media_id, self.entity_id
            )
            media_type = sourced_media.mime_type
            media_id = sourced_media.url

        extra = kwargs.get(ATTR_MEDIA_EXTRA, {})

        # Handle media supported by a known cast app
        if media_type == CAST_DOMAIN:
            try:
                app_data = json.loads(media_id)
                if metadata := extra.get("metadata"):
                    app_data["metadata"] = metadata
            except json.JSONDecodeError:
                _LOGGER.error("Invalid JSON in media_content_id")
                raise

            # Special handling for passed `app_id` parameter. This will only launch
            # an arbitrary cast app, generally for UX.
            if "app_id" in app_data:
                app_id = app_data.pop("app_id")
                _LOGGER.info("Starting Cast app by ID %s", app_id)
                await self.hass.async_add_executor_job(chromecast.start_app, app_id)
                if app_data:
                    _LOGGER.warning(
                        "Extra keys %s were ignored. Please use app_name to cast media",
                        app_data.keys(),
                    )
                return

            app_name = app_data.pop("app_name")
            try:
                await self.hass.async_add_executor_job(
                    quick_play, chromecast, app_name, app_data
                )
            except NotImplementedError:
                _LOGGER.error("App %s not supported", app_name)
            return

        # Try the cast platforms
        for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
            result = await platform.async_play_media(
                self.hass, self.entity_id, chromecast, media_type, media_id
            )
            if result:
                return

        # If media ID is a relative URL, we serve it from HA.
        media_id = async_process_play_media_url(self.hass, media_id)

        # Configure play command for when playing a HLS stream
        if is_hass_url(self.hass, media_id):
            parsed = yarl.URL(media_id)
            if parsed.path.startswith("/api/hls/"):
                extra = {
                    **extra,
                    "stream_type": "LIVE",
                    "media_info": {
                        "hlsVideoSegmentFormat": "fmp4",
                    },
                }
        elif (
            media_id.endswith(".m3u")
            or media_id.endswith(".m3u8")
            or media_id.endswith(".pls")
        ):
            try:
                playlist = await parse_playlist(self.hass, media_id)
                _LOGGER.debug(
                    "[%s %s] Playing item %s from playlist %s",
                    self.entity_id,
                    self._cast_info.friendly_name,
                    playlist[0].url,
                    media_id,
                )
                media_id = playlist[0].url
                if title := playlist[0].title:
                    extra = {
                        **extra,
                        "metadata": {"title": title},
                    }
            except PlaylistSupported as err:
                _LOGGER.debug(
                    "[%s %s] Playlist %s is supported: %s",
                    self.entity_id,
                    self._cast_info.friendly_name,
                    media_id,
                    err,
                )
            except PlaylistError as err:
                _LOGGER.warning(
                    "[%s %s] Failed to parse playlist %s: %s",
                    self.entity_id,
                    self._cast_info.friendly_name,
                    media_id,
                    err,
                )

        # Default to play with the default media receiver
        app_data = {"media_id": media_id, "media_type": media_type, **extra}
        _LOGGER.debug(
            "[%s %s] Playing %s with default_media_receiver",
            self.entity_id,
            self._cast_info.friendly_name,
            app_data,
        )
        await self.hass.async_add_executor_job(
            quick_play, chromecast, "default_media_receiver", app_data
        )