Can't call service that returns response

I am having trouble calling get_forecast on a weather entity. The service returns response data which I want to capture and parse.

I’ve compacted it into a simple example and inserted it into HelloWorld.

class HelloWorld(hass.Hass):

    def initialize(self):
        self.log("Hello from AppDaemon")
        self.get_weather()

    def get_weather(self):
        self.temp_forecast_entity = self.get_entity('weather.ksjc_daynight')
        self.log(f"Calling get_forecast on {self.temp_forecast_entity}")
        response=self.temp_forecast_entity.call_service(service='get_forecast', type='hourly', return_response=True)
        self.log(f"Got this response {response}")

The logs in Appdaemon look like this:

The homeassistan.log gets this message => Which made me add the return_response=True, but this didn’t help.

For good measure, I checked the service call does work and return data

I do not know the answer to your question, and it won’t solve it, but be aware get_forecast is deprecated. It will be removed at some point. Use get_forecasts (notice the s) instead.

OK, I’ve tried with and without the ‘s’. Same issue.

I just found another post with the same issue:

I’ve also tried calling services from different integrations and get the same issue.
None of the different calls in the code below work.
It seems that I cant call any service with Appdaemon that returns data

    def get_weather(self):
        self.temp_forecast_entity = self.get_entity('weather.ksjc_daynight')
        self.temp_forecast_entity = self.get_entity('weather.forecast_home')
        self.log(f"Calling get_forecast on {self.temp_forecast_entity}")
        #response=self.temp_forecast_entity.call_service(service='get_forecasts', type='hourly', return_response=True)
        #response=self.call_service('weather/get_forecast', entity_id = 'weather.ksjc_daynight', type = 'hourly', return_result=True)
        #response=self.call_service('weather/get_forecasts', entity_id = 'weather.forecast_home', type = 'hourly', return_response=True)
        response=self.call_service('soundtouchplus/preset_list', entity_id = 'media_player.soundtouch_kitchen', return_response=True)
        self.log(f"Got this response {response}")

@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!

Hi Todd

I am using Python but via the AppDaemon add on.
AppDaemon supplies a rich API for calling home assistant functions and I suspect something is going wrong inside that API wrapper. But I’ll see if I can use the information you provided and call it more directly.

Cheers!

I am not familiar with the AppDaemon specifics, but did a quick search and found the project on GitHub.

It appears that you are using the Entity.call_service method. Per the docs … “return_result(bool, option): If return_result is provided and set to True AD will attempt to wait for the result, and return it after execution”. Based on that, it SHOULD wait for a result to be returned; how long it waits before giving up is anybodys guess.

I think the error might be the slash between the domain and the service name; I think it should be a period (e.g. weather.get_forecasts) instead of a slash (e.g. weather/get_forecasts). The call_service method is doing a domain, _ = entity_id.split(".") to get the domain portion of the service name, and is expecting a period delimiter.

Try something like this:

response = self.call_service(
    'weather.get_forecasts', 
    entity_id = 'weather.forecast_home', 
    type = 'hourly', 
    return_result = True
)

or even:

response = self.temp_forecast_entity.call_service(
    'weather.get_forecasts', 
    entity_id = 'weather.forecast_home', 
    type = 'hourly', 
    return_result = True
)

If those don’t work, then it might be an issue with the type argument, as type is a special word in Python. In that case, you could do something like this:

mykwargs:dict = {
    'entity_id ': 'weather.forecast_home', 
    'type': 'hourly', 
    'return_result': True
}
response = self.temp_forecast_entity.call_service(
    'weather.get_forecasts', 
    **mykwargs
)

It should equate to the same call made in the Developer Tools \ Services utility:

service: weather.get_forecasts
data:
  entity_id: weather.forecast_home
  type: hourly

Hope it helps!

There seems top be two ways to call a service in Appdaemon

  1. Via the entity object
  2. Via the base class of the app (which is hass.Hass)

If calling via the object, you get the entity and then call its call_service method. In this case the service doesn’t need any additional path to the service, and it doesn’t need the entity_id.

self.temp_forecast_entity = self.get_entity('weather.ksjc_daynight')
response=self.temp_forecast_entity.call_service(service='get_forecasts', type='hourly', return_response=True)

Or by calling from the hass.Hass’s call_service method. Now you need to specify the full name of the service and the format needed is with “/” separator, not a period. And you need to specify the entity_id.

response=self.call_service('weather/get_forecasts', entity_id = 'weather.forecast_home', type = 'hourly', return_response=True)

I believe both of these examples are correct. Other variations such as return_result=True or using a period in the service path result in an error in Appdaemon log. These examples result in a Appdaemon Log Entry like this

2024-05-20 16:37:10.625349 WARNING HASS: Code: 500, error: 500 Internal Server Error Server got itself in trouble
2024-05-20 16:37:10.622865 WARNING HASS: Error calling Home Assistant service default/weather/get_forecasts

and a home assistant log like this

2024-05-20 16:37:10.605 ERROR (MainThread) [aiohttp.server] Error handling request
Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/aiohttp/web_protocol.py", line 452, in _handle_request
    resp = await request_handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/aiohttp/web_app.py", line 543, in _handle
    resp = await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/aiohttp/web_middlewares.py", line 114, in impl
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/http/security_filter.py", line 92, in security_filter_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/http/forwarded.py", line 83, in forwarded_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/http/request_context.py", line 26, in request_context_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/http/ban.py", line 88, in ban_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/aiohttp_session/__init__.py", line 199, in factory
    response = await handler(request)
               ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/http/auth.py", line 295, in auth_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/http/headers.py", line 32, in headers_middleware
    response = await handler(request)
               ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/http.py", line 73, in handle
    result = await handler(request, **request.match_info)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/api/__init__.py", line 406, in post
    await shield(
  File "/usr/src/homeassistant/homeassistant/core.py", line 2692, in async_call
    raise ServiceValidationError(
homeassistant.exceptions.ServiceValidationError: The service call requires responses and must be called with return_response=True

So my conclusion is that there is a bug the the Appdaemon API and I’ll file it on github to see if we can get a fix.

This isn’t a bug as such, it’s more of a known limitation, HASS added the ability to return values in a service a while ago but AppDaemon hasn’t yet been updated to handle it. AppDaemon is able to return values to service calls internally, it’s just not supported for calling HASS services.

It’s on the radar, but may require a rewrite of the HASS adaptor to use the stream instead of the older REST API.

Got it. Thanks for the response
It seems like the work around for now is to use a template sensor and point AppDaemon to the output sensor.

@aimc @jasebob
Does AppDaemon support calling a script? If so, could you call the service from a script, and return results that way?