@jasebob
Are you trying to call the service from TypeScript (front-end)? or from Python in an integration?
I have found that in order to call services that return service response data, you have to wrap the call in a script (for front-end calls anyway).
Here’s how I do it in TypeScript:
/**
* Calls the specified SoundTouchPlus service and returns response data that is generated by the
* service. The service is called via a script, as there is currently no way to return service
* response data from a call to "hass.callService()" (as of 2024/04/26).
*
* @param serviceRequest Service request instance that contains the service to call and its parameters.
* @returns Response data, in the form of a Record<string, any> (e.g. dictionary).
*/
public async CallServiceWithResponse(serviceRequest: ServiceCallRequest): Promise<string> {
try {
//console.log("%csoundtouchplus-service.CallServiceWithResponse()\n Calling service '%s' (with response)\n%s", "color: orange;", serviceRequest.service, JSON.stringify(serviceRequest, null, 2));
// call the service as a script.
const serviceResponse = await this.hass.connection.sendMessagePromise<ServiceCallResponse>({
type: "execute_script",
sequence: [{
"service": serviceRequest.domain + "." + serviceRequest.service,
"data": serviceRequest.serviceData,
"target": serviceRequest.target,
"response_variable": "service_result"
},
{
"stop": "done",
"response_variable": "service_result"
}]
});
//console.log("soundtouchplus-service.CallServiceWithResponse()\n Service Response:\n%s", JSON.stringify(serviceResponse.response));
// return the service response data or an empty dictionary if no response data was generated.
return JSON.stringify(serviceResponse.response)
} finally {
}
}
I can then call the above for any service that generates service response data - like so:
/**
* Retrieves the list of presets defined to the device.
*
* @param entityId Entity ID of the SoundTouchPlus device that will process the request (e.g. "media_player.soundtouch_livingroom").
* @param includeEmptySlots - True to include ALL preset slots (both empty and set); otherwise, False (default) to only include preset slots that have been set.
* @returns A PresetList object.
*/
public async PresetList(entityId: string, includeEmptySlots: boolean = true): Promise<PresetList> {
try {
// create service request.
const serviceRequest: ServiceCallRequest = {
domain: DOMAIN_SOUNDTOUCHPLUS,
service: 'preset_list',
serviceData: {
entity_id: entityId,
include_empty_slots: includeEmptySlots,
}
};
// call the service, and convert the response to a type.
const response = await this.CallServiceWithResponse(serviceRequest);
const responseObj = JSON.parse(response) as PresetList
return responseObj;
} finally {
}
}
The following is a Python excerpt from my SoundTouchPlus integration that is calling a service in my SpotifyPlus integration to return Track Favorites.
def _SpotifyPlusGetTrackFavorites(hass:HomeAssistant,
data:InstanceDataSoundTouchPlus,
playerName:str,
media_content_type:str|None,
media_content_id:str|None,
) -> Tuple[PlaylistPageSimplified, list[NavigateItem]]:
"""
Calls the spotifyPlus integration service "get_track_favorites", and returns the media and items results.
Args:
hass (HomeAssistant):
HomeAssistant instance.
data (InstanceDataSoundTouchPlus):
Component instance data that contains the SoundTouchClient instance.
playerName (str):
Name of the media player that is calling this method (for tracing purposes).
media_content_type (str):
Selected media content type in the media browser.
This value will be None upon the initial entry to the media browser.
media_content_id (str):
Selected media content id in the media browser.
This value will be None upon the initial entry to the media browser.
Returns:
A tuple of 2 objects:
- `TrackPageSaved` object that contains the results.
- list[NavigateItem] list of items that will be loaded to child nodes.
"""
# call SpotifyPlus integration service.
# this returns a dictionary of a partial user profile, as well as the items retrieved.
result:dict = run_coroutine_threadsafe(
hass.services.async_call(
DOMAIN_SPOTIFYPLUS,
'get_track_favorites',
{
"entity_id": data.OptionSpotifyMediaPlayerEntityId,
"limit": SPOTIFY_BROWSE_LIMIT,
"offset": 0
},
blocking=True, # wait for service to complete before returning
return_response=True # returns service response data.
), hass.loop
).result()
_logsi.LogDictionary(SILevel.Verbose, STAppMessages.MSG_SPOTIFYPLUS_RESULT_DICTIONARY % playerName, result, prettyPrint=True)
# convert results dictionary to managed code instances.
media:TrackPageSaved = TrackPageSaved(root=result.get("result", None))
if media is None:
raise MediaSourceNotFoundError(STAppMessages.MSG_SPOTIFYPLUS_RESULT_ITEMS_FORMAT_ERROR % (playerName, media_content_type))
mediaItems:list[Track] = media.GetTracks()
_logsi.LogArray(SILevel.Verbose, STAppMessages.MSG_SPOTIFYPLUS_RESULT_ITEMS % playerName, mediaItems)
userProfile:UserProfile = UserProfile(root=result.get("user_profile", None))
if userProfile is None:
raise MediaSourceNotFoundError(STAppMessages.MSG_SPOTIFYPLUS_USERPROFILE_FORMAT_ERROR % (playerName, media_content_type))
_logsi.LogObject(SILevel.Verbose, STAppMessages.MSG_SPOTIFYPLUS_USERPROFILE % playerName, userProfile, excludeNonPublic=True)
result = None
# verify that the soundtouch Spotify source userid matches the spotifyPlus integration
# userid that obtatined the results. if they don't match, then it's a problem because
# the soundtouch device won't be able to play it!
spotifySourceItem:SourceItem = _GetSpotifySourceItem(playerName, data, userProfile)
# build a list of soundtouchapi NavigateItems (which also contain ContentItem) to
# use in the child load process.
items:list[NavigateItem] = []
item:Track
for item in mediaItems:
ci:ContentItem = ContentItem(spotifySourceItem.Source, "uri", item.Uri, spotifySourceItem.SourceAccount, True, name=item.Name, containerArt=item.ImageUrl)
navItem:NavigateItem = NavigateItem(ci.Source, ci.SourceAccount, ci.Name, ci.TypeValue, contentItem=ci)
items.append(navItem)
return media, items
Hope it helps!