From 5d3d99dfbd7b6d2c4c8b9ad091c3dea58339bc12 Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Tue, 9 Dec 2025 00:51:06 -0600 Subject: [PATCH 1/7] return response working with optional services --- appdaemon/plugins/hass/hassapi.py | 12 ++++++++++-- appdaemon/plugins/hass/hassplugin.py | 9 ++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/appdaemon/plugins/hass/hassapi.py b/appdaemon/plugins/hass/hassapi.py index 6445bb997..aeb22403a 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, + 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. @@ -544,7 +552,7 @@ 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) + return await super().call_service(service, namespace, timeout=timeout, 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. diff --git a/appdaemon/plugins/hass/hassplugin.py b/appdaemon/plugins/hass/hassplugin.py index 0c7f0579f..7d1639658 100644 --- a/appdaemon/plugins/hass/hassplugin.py +++ b/appdaemon/plugins/hass/hassplugin.py @@ -769,6 +769,10 @@ 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} + # Set the return_response flag in the request from the service data + if "return_response" in data: + req["return_response"] = data.pop("return_response") + service_data = data.pop("service_data", {}) service_data.update(data) if service_data: @@ -783,9 +787,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 returning the response. req["return_response"] = True if target is None and entity_id is not None: From b14a739b53db6e056814a9a28dcf5eda596208dd Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Tue, 9 Dec 2025 00:51:54 -0600 Subject: [PATCH 2/7] added process_conversation and reload_conversation --- appdaemon/plugins/hass/hassapi.py | 124 +++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 3 deletions(-) diff --git a/appdaemon/plugins/hass/hassapi.py b/appdaemon/plugins/hass/hassapi.py index aeb22403a..622cc0445 100644 --- a/appdaemon/plugins/hass/hassapi.py +++ b/appdaemon/plugins/hass/hassapi.py @@ -465,9 +465,9 @@ async def call_service( self, service: str, namespace: str | None = None, - timeout: str | int | float | None = None, + timeout: str | int | float | None = None, # used by the sync_decorator callback: Callable[[Any], Any] | None = None, - **kwargs + **kwargs, ) -> Any: """Calls a Service within AppDaemon. @@ -552,7 +552,8 @@ async def call_service( """ # 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(service, namespace, timeout=timeout, callback=callback, **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. @@ -1668,3 +1669,120 @@ 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. + + This action is able to return + `response data `_. + The response is the same response as for the `/api/conversation/process API `_. + + 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, + ) From 0f3f9d42f2881fe56919f340a0fb89f436f19104 Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Sat, 13 Dec 2025 08:39:03 -0600 Subject: [PATCH 3/7] update history --- docs/HISTORY.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 70f97209b..2e24c0ebc 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 [Hass API](./HASS_API_REFERENCE.rst#api-usage) **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) From 150870888c4baa959527813187ba39efa5f219f5 Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Sat, 13 Dec 2025 11:33:47 -0600 Subject: [PATCH 4/7] docstrings --- appdaemon/plugins/hass/hassapi.py | 7 ++++++- appdaemon/plugins/hass/hassplugin.py | 12 ++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/appdaemon/plugins/hass/hassapi.py b/appdaemon/plugins/hass/hassapi.py index 622cc0445..9f3f7b87e 100644 --- a/appdaemon/plugins/hass/hassapi.py +++ b/appdaemon/plugins/hass/hassapi.py @@ -507,6 +507,10 @@ async def call_service( 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. @@ -1686,7 +1690,8 @@ def process_conversation( callback: ServiceCallback | None = None, return_response: bool = True, ) -> dict[str, Any]: - """Send a message to a conversation agent for processing. + """Send a message to a conversation agent for processing with the + `conversation.process action `_ This action is able to return `response data `_. diff --git a/appdaemon/plugins/hass/hassplugin.py b/appdaemon/plugins/hass/hassplugin.py index 7d1639658..8444519c5 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, ): @@ -745,6 +746,10 @@ async def call_plugin_service( 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,9 +774,8 @@ 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} - # Set the return_response flag in the request from the service data - if "return_response" in data: - req["return_response"] = data.pop("return_response") + if return_response is not None: + req["return_response"] = return_response service_data = data.pop("service_data", {}) service_data.update(data) @@ -792,7 +796,7 @@ async def call_plugin_service( # 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 returning the response. + # 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: From 5ade9c3d9fd87a1e0d095f9a968b5e1d81ee9635 Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Sat, 13 Dec 2025 11:39:00 -0600 Subject: [PATCH 5/7] docstring tweaks --- appdaemon/plugins/hass/hassapi.py | 5 +++-- appdaemon/plugins/hass/hassplugin.py | 4 ++-- docs/HISTORY.md | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/appdaemon/plugins/hass/hassapi.py b/appdaemon/plugins/hass/hassapi.py index 9f3f7b87e..497cf19ee 100644 --- a/appdaemon/plugins/hass/hassapi.py +++ b/appdaemon/plugins/hass/hassapi.py @@ -1693,9 +1693,10 @@ def process_conversation( """Send a message to a conversation agent for processing with the `conversation.process action `_ - This action is able to return + This action is able to return `response data `_. - The response is the same response as for the `/api/conversation/process API `_. + 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. diff --git a/appdaemon/plugins/hass/hassplugin.py b/appdaemon/plugins/hass/hassplugin.py index 8444519c5..c2790af1c 100644 --- a/appdaemon/plugins/hass/hassplugin.py +++ b/appdaemon/plugins/hass/hassplugin.py @@ -738,8 +738,8 @@ 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 diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 2e24c0ebc..4fcfc7640 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -8,7 +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 [Hass API](./HASS_API_REFERENCE.rst#api-usage) +- Added {py:meth}`~appdaemon.plugins.hass.hassapi.Hass.process_conversation` and {py:meth}`~appdaemon.plugins.hass.hassapi.Hass.reload_conversation` to the [Hass API](./HASS_API_REFERENCE.rst#api-usage). **Fixes** From 1a20a25d9be7e9f0393f1ce29954407dceb7c06e Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Sat, 13 Dec 2025 11:51:44 -0600 Subject: [PATCH 6/7] fixed link --- docs/HASS_API_REFERENCE.rst | 3 +++ docs/HISTORY.md | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) 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 4fcfc7640..7c31c1604 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -8,7 +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 [Hass API](./HASS_API_REFERENCE.rst#api-usage). +- 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** From 23d8001323677a90213e14f867dad7a9a7cb362b Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Sat, 13 Dec 2025 11:52:56 -0600 Subject: [PATCH 7/7] whitespace --- appdaemon/plugins/hass/hassapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appdaemon/plugins/hass/hassapi.py b/appdaemon/plugins/hass/hassapi.py index 497cf19ee..0aa994348 100644 --- a/appdaemon/plugins/hass/hassapi.py +++ b/appdaemon/plugins/hass/hassapi.py @@ -1692,7 +1692,7 @@ def process_conversation( ) -> 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