diff --git a/appdaemon/plugins/hass/hassapi.py b/appdaemon/plugins/hass/hassapi.py index 6445bb997..0aa994348 100644 --- a/appdaemon/plugins/hass/hassapi.py +++ b/appdaemon/plugins/hass/hassapi.py @@ -456,11 +456,19 @@ async def call_service( callback: ServiceCallback | None = None, hass_timeout: str | int | float | None = None, suppress_log_messages: bool = False, + return_response: bool | None = None, **data, ) -> Any: ... @utils.sync_decorator - async def call_service(self, *args, timeout: str | int | float | None = None, **kwargs) -> Any: + async def call_service( + self, + service: str, + namespace: str | None = None, + timeout: str | int | float | None = None, # used by the sync_decorator + callback: Callable[[Any], Any] | None = None, + **kwargs, + ) -> Any: """Calls a Service within AppDaemon. Services represent specific actions, and are generally registered by plugins or provided by AppDaemon itself. @@ -499,6 +507,10 @@ async def call_service(self, *args, timeout: str | int | float | None = None, ** Appdaemon will suppress logging of warnings for service calls to Home Assistant, specifically timeouts and non OK statuses. Use this flag and set it to ``True`` to suppress these log messages if you are performing your own error checking as described `here `__ + return_response (bool, optional): Indicates whether Home Assistant should return a response to the service + call. This is only supported for some services and Home Assistant will return an error if used with a + service that doesn't support it. If returning a response is required or optional (based on the service + definitions given by Home Assistant), this will automatically be set to ``True``. service_data (dict, optional): Used as an additional dictionary to pass arguments into the ``service_data`` field of the JSON that goes to Home Assistant. This is useful if you have a dictionary that you want to pass in that has a key like ``target`` which is otherwise used for the ``target`` argument. @@ -544,7 +556,8 @@ async def call_service(self, *args, timeout: str | int | float | None = None, ** """ # We just wrap the ADAPI.call_service method here to add some additional arguments and docstrings kwargs = utils.remove_literals(kwargs, (None,)) - return await super().call_service(*args, timeout=timeout, **kwargs) + # We intentionally don't pass the timeout kwarg here because it's applied by the sync_decorator + return await super().call_service(service, namespace, callback=callback, **kwargs) def get_service_info(self, service: str) -> dict | None: """Get some information about what kind of data the service expects to receive, which is helpful for debugging. @@ -1660,3 +1673,122 @@ def label_entities(self, label_name_or_id: str) -> list[str]: information. """ return self._template_command('label_entities', label_name_or_id) + + # Conversation + # https://developers.home-assistant.io/docs/intent_conversation_api + + def process_conversation( + self, + text: str, + language: str | None = None, + agent_id: str | None = None, + conversation_id: str | None = None, + *, + namespace: str | None = None, + timeout: str | int | float | None = None, + hass_timeout: str | int | float | None = None, + callback: ServiceCallback | None = None, + return_response: bool = True, + ) -> dict[str, Any]: + """Send a message to a conversation agent for processing with the + `conversation.process action `_ + + This action is able to return + `response data `_. + The response is the same as the one returned by the `/api/conversation/process` API; see + ``_ for details. + + See the docs on the `conversation integration `__ for + more information. + + Args: + text (str): Transcribed text input to send to the conversation agent. + language (str, optional): Language of the text. Defaults to None. + agent_id (str, optional): ID of conversation agent. The conversation agent is the brains of the assistant. + It processes the incoming text commands. Defaults to None. + conversation_id (str, optional): ID of a new or previous conversation. Will continue an old conversation + or start a new one. Defaults to None. + namespace (str, optional): If provided, changes the namespace for the service call. Defaults to the current + namespace of the app, so it's safe to ignore this parameter most of the time. See the section on + `namespaces `__ for a detailed description. + timeout (str | int | float, optional): Timeout for the app thread to wait for a response from the main + thread. + hass_timeout (str | int | float, optional): Timeout for AppDaemon waiting on a response from Home Assistant + to respond to the backup request. Cannot be set lower than the timeout value. + callback (ServiceCallback, optional): Function to call with the results of the request. + return_response (bool, optional): Whether Home Assistant should return a response to the service call. Even + if it's False, Home Assistant will still respond with an acknowledgement. Defaults to True + + Returns: + dict: The response from the conversation agent. See the docs on + `conversation response `_ + for more information. + + Examples: + Extracting the text of the speech response, continuation flag, and conversation ID: + + >>> full_response = self.process_conversation("Hello world!") + >>> match full_response: + ... case {'success': True, 'result': dict(result)}: + ... match result['response']: + ... case { + ... 'response': dict(response), + ... 'continue_conversation': bool(continue_conv), + ... 'conversation_id': str(conv_id), + ... }: + ... speech: str = response['speech']['plain']['speech'] + ... self.log(speech, ascii_encode=False) + ... self.log(continue_conv) + ... self.log(conv_id) + + Extracting entity IDs from a successful action response: + + >>> full_response = self.process_conversation("Turn on the living room lights") + >>> match full_response: + ... case {'success': True, 'result': dict(result)}: + ... match result['response']: + ... case {'response': {'data': {'success': list(entities)}}}: + ... eids = [e['id'] for e in entities] + ... self.log(eids) + """ + return self.call_service( + service='conversation/process', + text=text, + language=language, + agent_id=agent_id, + conversation_id=conversation_id, + namespace=namespace if namespace is not None else self.namespace, + timeout=timeout, + callback=callback, + hass_timeout=hass_timeout, + return_response=return_response, + ) + + def reload_conversation( + self, + language: str | None = None, + agent_id: str | None = None, + *, + namespace: str | None = None, + ) -> dict[str, Any]: + """Reload the intent cache for a conversation agent. + + See the docs on the `conversation integration `__ for + more information. + + Args: + language (str, optional): Language to clear intent cache for. No value clears all languages. Defaults to None. + agent_id (str, optional): ID of conversation agent. Defaults to the built-in Home Assistant agent. + namespace (str, optional): If provided, changes the namespace for the service call. Defaults to the current + namespace of the app, so it's safe to ignore this parameter most of the time. See the section on + `namespaces `__ for a detailed description. + + Returns: + dict: The acknowledgement response from Home Assistant. + """ + return self.call_service( + service='conversation/reload', + language=language, + agent_id=agent_id, + namespace=namespace if namespace is not None else self.namespace, + ) diff --git a/appdaemon/plugins/hass/hassplugin.py b/appdaemon/plugins/hass/hassplugin.py index 0c7f0579f..c2790af1c 100644 --- a/appdaemon/plugins/hass/hassplugin.py +++ b/appdaemon/plugins/hass/hassplugin.py @@ -723,6 +723,7 @@ async def call_plugin_service( target: str | dict | None = None, entity_id: str | list[str] | None = None, # Maintained for legacy compatibility hass_timeout: str | int | float | None = None, + return_response: bool | None = None, suppress_log_messages: bool = False, **data, ): @@ -737,14 +738,18 @@ async def call_plugin_service( service (str): Name of the service to call target (str | dict | None, optional): Target of the service. Defaults to None. If the ``entity_id`` argument is not used, then the value of the ``target`` argument is used directly. - entity_id (str | list[str] | None, optional): Entity ID to target with the service call. Seems to be a - legacy way . Defaults to None. + entity_id (str | list[str] | None, optional): Entity ID to target with the service call. This argument is + maintained for legacy compatibility. Defaults to None. hass_timeout (str | int | float, optional): Sets the amount of time to wait for a response from Home Assistant. If no value is specified, the default timeout is 10s. The default value can be changed using the ``ws_timeout`` setting the in the Hass plugin configuration in ``appdaemon.yaml``. Even if no data is returned from the service call, Home Assistant will still send an acknowledgement back to AppDaemon, which this timeout applies to. Note that this is separate from the ``timeout``. If ``timeout`` is shorter than this one, it will trigger before this one does. + return_response (bool, optional): Indicates whether Home Assistant should return a response to the service + call. This is only supported for some services and Home Assistant will return an error if used with a + service that doesn't support it. If returning a response is required or optional (based on the service + definitions given by Home Assistant), this will automatically be set to ``True``. suppress_log_messages (bool, optional): If this is set to ``True``, Appdaemon will suppress logging of warnings for service calls to Home Assistant, specifically timeouts and non OK statuses. Use this flag and set it to ``True`` to suppress these log messages if you are performing your own error checking as @@ -769,6 +774,9 @@ async def call_plugin_service( # https://developers.home-assistant.io/docs/api/websocket#calling-a-service-action req: dict[str, Any] = {"type": "call_service", "domain": domain, "service": service} + if return_response is not None: + req["return_response"] = return_response + service_data = data.pop("service_data", {}) service_data.update(data) if service_data: @@ -783,9 +791,12 @@ async def call_plugin_service( for prop, val in info.items() # get each of the properties } - # Set the return_response flag if doing so is not optional match service_properties: case {"response": {"optional": False}}: + # Force the return_response flag if doing so is not optional + req["return_response"] = True + case {"response": {"optional": True}} if "return_response" not in req: + # If the response is optional, but not set above, default to return_response=True. req["return_response"] = True if target is None and entity_id is not None: diff --git a/docs/HASS_API_REFERENCE.rst b/docs/HASS_API_REFERENCE.rst index 28e6f543b..d2ca02dee 100644 --- a/docs/HASS_API_REFERENCE.rst +++ b/docs/HASS_API_REFERENCE.rst @@ -255,6 +255,9 @@ Example to wait for an input button before starting AppDaemon service_data: entity_id: input_button.start_appdaemon # example entity + +.. _hass-api-usage: + API Usage --------- diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 70f97209b..7c31c1604 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -8,6 +8,7 @@ - Add request context logging for failed HASS calls - contributed by [ekutner](https://github.com/ekutner) - Reload modified apps on SIGUSR2 - contributed by [chatziko](https://github.com/chatziko) - Using urlib to create endpoints from URLs - contributed by [cebtenzzre](https://github.com/cebtenzzre) +- Added {py:meth}`~appdaemon.plugins.hass.hassapi.Hass.process_conversation` and {py:meth}`~appdaemon.plugins.hass.hassapi.Hass.reload_conversation` to the {ref}`Hass API `. **Fixes** @@ -17,8 +18,8 @@ - Fix for connecting to Home Assistant with https - Fix for persistent namespaces in Python 3.12 - Better error handling for receiving huge websocket messages in the Hass plugin -- Fix for matching in get_history() - contributed by [cebtenzzre](https://github.com/cebtenzzre) -- Fix set_state() error handling - contributed by [cebtenzzre](https://github.com/cebtenzzre) +- Fix for matching in {py:meth}`~appdaemon.plugins.hass.hassapi.Hass.get_history` - contributed by [cebtenzzre](https://github.com/cebtenzzre) +- Fix {py:meth}`~appdaemon.state.State.set_state` error handling - contributed by [cebtenzzre](https://github.com/cebtenzzre) - Fix production mode and scheduler race - contributed by [cebtenzzre](https://github.com/cebtenzzre) - Fix scheduler crash - contributed by [cebtenzzre](https://github.com/cebtenzzre) - Fix startup when no plugins are configured - contributed by [cebtenzzre](https://github.com/cebtenzzre) @@ -66,7 +67,7 @@ None - Reverted discarding of events during app initialize methods to pre-4.5 by default and added an option to turn it on if required (should fix run_in() calls with a delay of 0 during initialize, as well as listen_state() with a duration and immediate=True) - Fixed logic in presence/person constraints - Fixed logic in calling services from HA so that things like `input_number/set_value` work with entities in the `number` domain -- Fixed `get_history` for boolean objects +- Fixed {py:meth}`~appdaemon.plugins.hass.hassapi.Hass.get_history` for boolean objects - Fixed config models to allow custom plugins - Fixed a bug causing spurious state refreshes - contributed by [FredericMa](https://github.com/FredericMa)