Skip to content

Commit ef011e0

Browse files
committed
[ 1.0.120 ] * Added service play_url_dlna. Play media from the given URL via the Bose DLNA API. This also allows you to set source-specific metadata (artist, album, track, and cover art url) for the playing UPNP content. Note that only HTTP URL's are supported (HTTPS is not, due to Bose DLNA limitations).
* Updated service `play_url` to update HA state after processing, as state changes were not updating in a timely manner (or at all in some instances) after the service completed. * Updated underlying `bosesoundtouchapi` package requirement to version 1.0.79.
1 parent d997465 commit ef011e0

File tree

8 files changed

+326
-15
lines changed

8 files changed

+326
-15
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ Change are listed in reverse chronological order (newest to oldest).
66

77
<span class="changelog">
88

9+
###### [ 1.0.120 ] - 2025/06/06
10+
11+
* Added service `play_url_dlna`. Play media from the given URL via the Bose DLNA API. This also allows you to set source-specific metadata (artist, album, track, and cover art url) for the playing UPNP content. Note that only HTTP URL's are supported (HTTPS is not, due to Bose DLNA limitations).
12+
* Updated service `play_url` to update HA state after processing, as state changes were not updating in a timely manner (or at all in some instances) after the service completed.
13+
* Updated underlying `bosesoundtouchapi` package requirement to version 1.0.79.
14+
915
###### [ 1.0.119 ] - 2025/05/22
1016

1117
* v1.0.118 release was FUBAR in github; updating release number by 1.

custom_components/soundtouchplus/__init__.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
SERVICE_PLAY_HANDOFF = "play_handoff"
8585
SERVICE_PLAY_TTS = "play_tts"
8686
SERVICE_PLAY_URL = "play_url"
87+
SERVICE_PLAY_URL_DLNA = "play_url_dlna"
8788
SERVICE_PRESET_LIST = "preset_list"
8889
SERVICE_PRESET_REMOVE = "preset_remove"
8990
SERVICE_REBOOT_DEVICE = "reboot_device"
@@ -226,6 +227,19 @@
226227
}
227228
)
228229

230+
SERVICE_PLAY_URL_DLNA_SCHEMA = vol.Schema(
231+
{
232+
vol.Required("entity_id"): cv.entity_id,
233+
vol.Required("url"): cv.string,
234+
vol.Optional("artist"): cv.string,
235+
vol.Optional("album"): cv.string,
236+
vol.Optional("track"): cv.string,
237+
vol.Optional("art_url"): cv.string,
238+
vol.Optional("update_now_playing_status", default=True): cv.boolean,
239+
vol.Optional("delay", default=1): vol.All(vol.Range(min=0,max=10))
240+
}
241+
)
242+
229243
SERVICE_PRESET_LIST_SCHEMA = vol.Schema(
230244
{
231245
vol.Required("entity_id"): cv.entity_id,
@@ -532,6 +546,17 @@ async def service_handle_entity(service:ServiceCall) -> None:
532546
_logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (service.service, entity.name))
533547
await hass.async_add_executor_job(entity.service_play_url, url, artist, album, track, volume_level, app_key, get_metadata_from_url_file)
534548

549+
elif service.service == SERVICE_PLAY_URL_DLNA:
550+
url = service.data.get("url")
551+
artist = service.data.get("artist")
552+
album = service.data.get("album")
553+
track = service.data.get("track")
554+
art_url = service.data.get("art_url")
555+
update_now_playing_status = service.data.get("update_now_playing_status")
556+
delay = service.data.get("delay")
557+
_logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (service.service, entity.name))
558+
await hass.async_add_executor_job(entity.service_play_url_dlna, url, artist, album, track, art_url, update_now_playing_status, delay)
559+
535560
elif service.service == SERVICE_PRESET_REMOVE:
536561
preset_id = service.data.get("preset_id")
537562
_logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (service.service, entity.name))
@@ -954,6 +979,15 @@ def _GetEntityFromServiceData(hass:HomeAssistant, service:ServiceCall, field_id:
954979
supports_response=SupportsResponse.NONE,
955980
)
956981

982+
_logsi.LogObject(SILevel.Verbose, STAppMessages.MSG_SERVICE_REQUEST_REGISTER % SERVICE_PLAY_URL_DLNA, SERVICE_PLAY_URL_DLNA_SCHEMA)
983+
hass.services.async_register(
984+
DOMAIN,
985+
SERVICE_PLAY_URL_DLNA,
986+
service_handle_entity,
987+
schema=SERVICE_PLAY_URL_DLNA_SCHEMA,
988+
supports_response=SupportsResponse.NONE,
989+
)
990+
957991
_logsi.LogObject(SILevel.Verbose, STAppMessages.MSG_SERVICE_REQUEST_REGISTER % SERVICE_PRESET_LIST, SERVICE_PRESET_LIST_SCHEMA)
958992
hass.services.async_register(
959993
DOMAIN,

custom_components/soundtouchplus/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"issue_tracker": "https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/issues",
1313
"loggers": [ "bosesoundtouchapi" ],
1414
"requirements": [
15-
"bosesoundtouchapi==1.0.77",
15+
"bosesoundtouchapi==1.0.79",
1616
"oauthlib>=3.2.2",
1717
"platformdirs>=4.1.0",
1818
"requests>=2.31.0",

custom_components/soundtouchplus/media_player.py

Lines changed: 134 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
import datetime as dt
2323
from functools import partial
2424
import logging
25+
from os import path
2526
import re
2627
from typing import Any
2728
import urllib.parse
29+
from urllib.parse import unquote
2830
from xml.etree import ElementTree
2931
from xml.etree.ElementTree import Element
3032

@@ -909,9 +911,27 @@ def play_media(self, media_type:MediaType|str, media_id:str, **kwargs: Any) -> N
909911

910912
# is the media an http or https url?
911913
elif re.match(r"http[s]?://", media_id):
912-
913-
_logsi.LogVerbose("'%s': MediaPlayer play_media detected URL media: Url='%s'", self.name, media_id)
914-
self._client.PlayUrl(media_id, artist=announceValue, album=announceValue, getMetaDataFromUrlFile=True, volumeLevel=0)
914+
915+
# get the track name portion of the url.
916+
# example: media_id = "http://192.168.1.248:8123/media/local/01%20The%20First%20Noel.mp3?authSig=ey...nO4"
917+
# trackName = "01 The First Noel.mp3"
918+
trackName:str = self._GetUrlFilename(media_id)
919+
920+
# always use the PlayUrl service, as it supports both HTTP and HTTPS url's.
921+
_logsi.LogVerbose("'%s': MediaPlayer play_media detected URL Notification media: Url='%s'", self.name, media_id)
922+
self._client.PlayUrl(media_id, artist=announceValue, album=announceValue, track=trackName, getMetaDataFromUrlFile=True, volumeLevel=0)
923+
924+
# commented this code, as most Radio Stations are HTTPS format!!!
925+
# for announcements, we will use the `PlayUrl` service; otherwise, use the `PlayUrlDlna` service.
926+
# if announce:
927+
# _logsi.LogVerbose("'%s': MediaPlayer play_media detected URL Notification media: Url='%s'", self.name, media_id)
928+
# self._client.PlayUrl(media_id, artist=announceValue, album=announceValue, getMetaDataFromUrlFile=True, volumeLevel=0)
929+
# else:
930+
# _logsi.LogVerbose("'%s': MediaPlayer play_media detected URL DLNA media: Url='%s'", self.name, media_id)
931+
# self._client.PlayUrlDlna(media_id, artist="", album="", track=trackName, artUrl="", updateNowPlayingStatus=True)
932+
933+
# inform Home Assistant of the status update.
934+
self.schedule_update_ha_state(force_refresh=False)
915935

916936
# is the media a spotify uri?
917937
elif re.match(r"spotify:", media_id):
@@ -1485,6 +1505,38 @@ def _FindEntityIdFromClientDeviceId(self, deviceId:str, serviceName:str) -> str:
14851505
return entity_id
14861506

14871507

1508+
def _GetUrlFilename(self, url:str):
1509+
"""
1510+
Returns the filename portion of a media_id url.
1511+
1512+
Args:
1513+
url (str):
1514+
media_id url to parse for the file name.
1515+
1516+
Returns:
1517+
The file name portion of the url if found; otherwise, an empty string.
1518+
"""
1519+
try:
1520+
1521+
# get the filename portion of the media_id url.
1522+
# example:
1523+
# media_id = "http://192.168.1.248:8123/media/local/01%20The%20First%20Noel.mp3?authSig=ey...nO4"
1524+
# fileName = "01 The First Noel.mp3"
1525+
1526+
fragment_removed = url.split("#")[0] # keep to left of first #
1527+
query_string_removed = fragment_removed.split("?")[0]
1528+
scheme_removed = query_string_removed.split("://")[-1].split(":")[-1]
1529+
if scheme_removed.find("/") == -1:
1530+
return ""
1531+
filename:str = path.basename(scheme_removed)
1532+
result = unquote(filename)
1533+
return result
1534+
1535+
except Exception as ex:
1536+
# ignore exceptions.
1537+
return ""
1538+
1539+
14881540
def _GetSourceItemByTitle(self, title:str) -> SourceItem:
14891541
"""
14901542
Returns a `SourceItem` instance for the given source title value
@@ -2275,7 +2327,9 @@ def service_play_url(
22752327
getMetadataFromUrlFile:bool,
22762328
) -> None:
22772329
"""
2278-
Play media content from a URL on a SoundTouch device.
2330+
Plays media from the given URL as a notification message, interrupting the currently playing
2331+
media to play the specified url. The currently playing will then resume playing once play of
2332+
the specified URL is complete.
22792333
22802334
Args:
22812335
url (str):
@@ -2310,11 +2364,86 @@ def service_play_url(
23102364
apiMethodParms.AppendKeyValue("volumeLevel", volumeLevel)
23112365
apiMethodParms.AppendKeyValue("appKey", appKey)
23122366
apiMethodParms.AppendKeyValue("getMetadataFromUrlFile", getMetadataFromUrlFile)
2313-
_logsi.LogMethodParmList(SILevel.Verbose, "SoundTouch Play URL Service", apiMethodParms)
2367+
_logsi.LogMethodParmList(SILevel.Verbose, "SoundTouch Play URL Notification Service", apiMethodParms)
23142368

23152369
# play url.
23162370
self.data.client.PlayUrl(url, artist, album, track, volumeLevel, appKey, getMetadataFromUrlFile)
23172371

2372+
# inform Home Assistant of the status update.
2373+
self.schedule_update_ha_state(force_refresh=False)
2374+
2375+
# the following exceptions have already been logged, so we just need to
2376+
# pass them back to HA for display in the log (or service UI).
2377+
except SoundTouchError as ex:
2378+
raise HomeAssistantError(ex.Message)
2379+
2380+
finally:
2381+
2382+
# trace.
2383+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
2384+
2385+
2386+
def service_play_url_dlna(
2387+
self,
2388+
url:str,
2389+
artist:str,
2390+
album:str,
2391+
track:str,
2392+
artUrl:str,
2393+
updateNowPlayingStatus:bool,
2394+
delay:int,
2395+
) -> None:
2396+
"""
2397+
Plays media from the given URL via the Bose DLNA API.
2398+
2399+
Args:
2400+
url (str):
2401+
The url to play.
2402+
Note that HTTPS URL's are not supported by this service due to DLNA restrictions.
2403+
artist (str):
2404+
The message text that will appear in the NowPlaying Artist node for source-specific nowPlaying information.
2405+
Default is "Unknown Artist"
2406+
album (str):
2407+
The message text that will appear in the NowPlaying Album node, for source-specific nowPlaying information.
2408+
Default is "Unknown Album"
2409+
track (str):
2410+
The message text that will appear in the NowPlaying Track node, for source-specific nowPlaying information.
2411+
Default is "Unknown Track"
2412+
artUrl (str):
2413+
A url link to a cover art image that represents the URL, for source-specific nowPlaying information.
2414+
Default is None.
2415+
updateNowPlayingStatus (bool):
2416+
True (default) to update the source-specific nowPlaying information;
2417+
False to not update the source-specific nowPlaying information.
2418+
delay (int):
2419+
Time delay (in seconds) to wait AFTER sending the play next track request if
2420+
the currently playing media is a notification source.
2421+
This delay will give the device time to process the change before another
2422+
command is accepted.
2423+
Default is 1; value range is 0 - 10.
2424+
"""
2425+
apiMethodName:str = 'service_play_url'
2426+
apiMethodParms:SIMethodParmListContext = None
2427+
2428+
try:
2429+
2430+
# trace.
2431+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
2432+
apiMethodParms.AppendKeyValue("url", url)
2433+
apiMethodParms.AppendKeyValue("artist", artist)
2434+
apiMethodParms.AppendKeyValue("album", album)
2435+
apiMethodParms.AppendKeyValue("track", track)
2436+
apiMethodParms.AppendKeyValue("artUrl", artUrl)
2437+
apiMethodParms.AppendKeyValue("updateNowPlayingStatus", updateNowPlayingStatus)
2438+
apiMethodParms.AppendKeyValue("delay", delay)
2439+
_logsi.LogMethodParmList(SILevel.Verbose, "SoundTouch Play URL DLNA Service", apiMethodParms)
2440+
2441+
# play url.
2442+
self.data.client.PlayUrlDlna(url, artist, album, track, artUrl, updateNowPlayingStatus, delay)
2443+
2444+
# inform Home Assistant of the status update.
2445+
self.schedule_update_ha_state(force_refresh=False)
2446+
23182447
# the following exceptions have already been logged, so we just need to
23192448
# pass them back to HA for display in the log (or service UI).
23202449
except SoundTouchError as ex:

custom_components/soundtouchplus/services.yaml

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ play_tts:
400400
volume_level:
401401
name: Volume Level
402402
description: The temporary volume level that will be used when the message is played. Specify a value of zero to play at the current volume. Default is zero.
403-
example: 50
403+
example: 10
404404
required: false
405405
selector:
406406
number:
@@ -418,8 +418,8 @@ play_tts:
418418
text:
419419

420420
play_url:
421-
name: Play URL Media
422-
description: Play media content from a URL on a SoundTouch device.
421+
name: Play URL Notification
422+
description: Play media from the given URL as a notification message, interrupting the currently playing media to play the specified url. The currently playing will then resume playing once play of the specified URL is complete.
423423
fields:
424424
entity_id:
425425
name: Entity ID
@@ -461,7 +461,7 @@ play_url:
461461
volume_level:
462462
name: Volume Level
463463
description: The temporary volume level that will be used when the media is played. Specify a value of zero to play at the current volume. Default is zero.
464-
example: 50
464+
example: 10
465465
required: false
466466
selector:
467467
number:
@@ -485,6 +485,72 @@ play_url:
485485
selector:
486486
boolean:
487487

488+
play_url_dlna:
489+
name: Play URL DLNA
490+
description: Play media from the given URL via the Bose DLNA API.
491+
fields:
492+
entity_id:
493+
name: Entity ID
494+
description: Entity ID of the SoundTouchPlus device that will process the request.
495+
example: "media_player.soundtouch_livingroom"
496+
required: true
497+
selector:
498+
entity:
499+
integration: soundtouchplus
500+
domain: media_player
501+
url:
502+
name: URL
503+
description: The url to play; HTTPS URL's are not supported by this service due to DLNA restrictions.
504+
example: "http://edge-bauerall-01-gos2.sharp-stream.com/ghr70s.aac"
505+
required: true
506+
selector:
507+
text:
508+
artist:
509+
name: Artist Status Text
510+
description: The message text that will appear in the NowPlaying Artist node for source-specific nowPlaying information; if omitted, default is "Unknown Artist".
511+
example: "Greatest Hits Radio"
512+
required: false
513+
selector:
514+
text:
515+
album:
516+
name: Album Status Text
517+
description: The message text that will appear in the NowPlaying Album node for source-specific nowPlaying information; if omitted, default is "Unknown Album".
518+
example: "70's Classic Hits"
519+
required: false
520+
selector:
521+
text:
522+
track:
523+
name: Track Status Text
524+
description: The message text that will appear in the NowPlaying Track node for source-specific nowPlaying information; if omitted, default is "Unknown Track".
525+
example: "ghr70s.aac"
526+
required: false
527+
selector:
528+
text:
529+
art_url:
530+
name: Cover Artwork URL
531+
description: A url link to a cover art image that represents the URL for source-specific nowPlaying information; if omitted, default is None.
532+
example: "https://image-cdn-ak.spotifycdn.com/image/ab67706c0000da849d37dd221d8aa1b35c545057"
533+
required: false
534+
selector:
535+
text:
536+
update_now_playing_status:
537+
name: Update NowPlaying Status?
538+
description: True (default) to update the source-specific nowPlaying information; False to not update the source-specific nowPlaying information.
539+
example: "true"
540+
required: false
541+
selector:
542+
boolean:
543+
delay:
544+
name: Command Delay
545+
description: Time delay (in seconds) to wait AFTER sending the play next track request if the currently playing media is a notification source. This delay will give the device time to process the change before another command is accepted. Default is 1; value range is 0 - 10.
546+
example: "1"
547+
required: false
548+
selector:
549+
number:
550+
min: 0
551+
max: 10
552+
mode: box
553+
488554
preset_list:
489555
name: Get Preset List
490556
description: Retrieves the list of presets defined to the device.

0 commit comments

Comments
 (0)