Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 134 additions & 2 deletions appdaemon/plugins/hass/hassapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 <APPGUIDE.html#some-notes-on-service-calls>`__
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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 <https://www.home-assistant.io/integrations/conversation/#action-conversationprocess>`_

This action is able to return
`response data <https://www.home-assistant.io/docs/scripts/perform-actions/#use-templates-to-handle-response-data>`_.
The response is the same as the one returned by the `/api/conversation/process` API; see
`<https://developers.home-assistant.io/docs/intent_conversation_api#conversation-response>`_ for details.

See the docs on the `conversation integration <https://www.home-assistant.io/integrations/conversation/>`__ 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 <APPGUIDE.html#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 <https://developers.home-assistant.io/docs/intent_conversation_api/#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 <https://www.home-assistant.io/integrations/conversation/>`__ 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 <APPGUIDE.html#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,
)
17 changes: 14 additions & 3 deletions appdaemon/plugins/hass/hassplugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
):
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions docs/HASS_API_REFERENCE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------

Expand Down
7 changes: 4 additions & 3 deletions docs/HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <hass-api-usage>`.

**Fixes**

Expand All @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
Loading