Hass Plugin/API
===============
About
-----
Hass Plugin
~~~~~~~~~~~
The `Hass` plugin connects to Home Assistant using the
`websocket API `_ and maintains this connection while
AppDaemon is running. In addition, it maintains an HTTP session because some functionality is only available via the
`REST API `_. If the connection is lost, the plugin will gracefully
attempt to reconnect every 5s until it succeeds, any apps that are using the `Hass` API will be stopped and restarted
when the connection is re-established.
Hass API
~~~~~~~~
The :py:class:`Hass ` API is an interface layer that makes it easy for users to
interact with the `Hass` plugin. In addition to all the methods of the ``ADAPI``, it provides many methods that are
specific to Home Assistant. Most of these methods simply wrap calling services with some logic that make them more
convenient to use.
Plugin Configuration
--------------------
The `Hass` plugin must be configured in the ``appdaemon.yaml`` file under the ``plugins`` section in order for it to
connect to Home Assistant. This example shows where it fits into the overall configuration file.
.. code:: yaml
# conf/appdaemon.yaml
appdaemon:
... # other AppDaemon config here
plugins:
HASS: # This is the name of the plugin, it can be anything
type: hass # required
ha_url: ... # required
token: ... # required
... # other Hass plugin config options here
Configuration Options
~~~~~~~~~~~~~~~~~~~~~
This is the full list of configuration options available for the `Hass` plugin.
.. list-table:: HASS Plugin Configuration Options
:header-rows: 1
:widths: 20 6 80
* - **Key**
- **Note**
- **Description**
* - ``type``
- required
- This must be declared and it must be the exact value ``hass``.
* - ``ha_url``
- required
- URL to a Home Assistant instance, must include correct port and scheme (``http://`` or ``https://``)
* - ``token``
- required
- Long-lived token for for authentication with Home Assistant. See the
`section on authentication <#authentication>`_ for more information on how to set it up.
* - ``ha_key``
- deprecated
- Use ``token`` instead
* - ``retry_secs``
- optional
- Time to sleep between connection attempts. Defaults to 5 seconds.
* - ``cert_verify``
- optional
- Flag for adding an SSL context around the ``aiohttp.ClientSession``. Set to ``False`` to disable (e.g., with internal IPs)
* - ``cert_path``
- optional
- Path to the SSL certificate file. This is only used if ``cert_verify`` is set to ``True``.
* - ``api_port``
- optional
- Port the AppDaemon RESTful API will listen on. If not specified, API is disabled
* - ``ws_timeout``
- optional
- Timeout for waiting for Home Assistant response from the websocket API. This is the time between when a websocket
message is first sent and when Home Assistant responds with some kind of acknowledgement/result. Config values
are parsed with :py:func:`parse_timedelta `. Defaults to 10 seconds.
* - ``suppress_log_messages``
- optional
- If ``true``, suppress log messages related to :py:meth:`call_service `.
Defaults to ``false``.
* - ``app_init_delay``
- optional
- Delay in seconds before initializing apps and listening for events
* - ``appdaemon_startup_conditions``
- optional
- See the `startup control section <#startup-control>`_ for more information.
* - ``plugin_startup_conditions``
- optional
- See the `startup control section <#startup-control>`_ for more information.
Authentication
~~~~~~~~~~~~~~
The `Hass` plugin needs a long-lived access token to authenticate with Home Assistant over the websocket. This is
provided to AppDaemon by the ``token`` directive in the plugin configuration.
To create a long-lived access token, use the following steps:
1. Login as the user that you want to create the token for and open the user profile. The profile is found by clicking
the icon next to the ``Home Assistant`` label to the left of the web ui when the burger menu is clicked:
.. figure:: images/Profile.png
:alt: Profile
2. At the bottom of the user profile is the Long-Lived Access Tokens section. Click on "Create Token"
.. figure:: images/create_token.png
:alt: Create Token
This will pop up a dialog that asks you for the name of the token - this can be anything, it's just to remind you what
the token was created for - ``AppDaemon`` is as good a name as any. When you are done click ``OK``
.. figure:: images/popup.png
:alt: Popup
3. A new dialog will popup with the token itself showing:
.. figure:: images/token.png
:alt: Token
Copy this string and add it as the argument of the ``token`` directive in your HASS Plugin section:
.. code:: yaml
token: ABCDEF
A real token will be a lot longer than this and will consist of a string of random letters and numbers. For example:
``eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIwZmRkYmE0YTM0MTY0...``
4. A reference to your new token will be shown in the Long-Lived tokens section, and you can revoke access via this
token at any time by pressing the delete icon. The token will last for 10 years.
.. figure:: images/list.png
:alt: List
Startup Control
~~~~~~~~~~~~~~~
The `Hass` plugin has the ability to pause startup until various criteria have been met. This can be useful for
preventing apps that depend on certain entities or services from starting before they are available. AppDaemon will not
mark the plugin as ready until all of these conditions have been met, which prevents any apps that depend on the plugin
from being started. Each condition only has to be met once for it to be considered satisfied.
When the plugin first starts with AppDaemon itself, it will check the conditions in the ``appdaemon_startup_conditions``
key before starting any apps. If the connection to Home Assistant is broken and re-established, it will check the
conditions in the ``plugin_startup_conditions`` key before starting any apps.
.. admonition:: Starting Event
:class: note
If/when the `Hass` plugin reconnects to Home Assistant, it will wait for the ``homeassistant_started`` event before
starting any of the apps that use the `Hass` API. Home Assistant will accept connections very early as it's
starting, even before some fundamental components have been loaded, which causes most apps to somehow fail without
waiting for this event. This is the same event that the Home Assistant web UI waits for to indicate readiness.
.. code:: yaml
# conf/appdaemon.yaml
appdaemon:
... # other AppDaemon config here
plugins:
HASS:
type: hass # required
ha_url: ... # required
token: ... # required
... # other Hass plugin config options here
appdaemon_startup_conditions:
delay: ...
state: ...
event: ...
plugin_startup_conditions:
delay: ...
state: ...
event: ...
delay
^^^^^
Delay startup for a number of seconds, for example:
.. code:: yaml
delay: 10 # delays for 10s
state
^^^^^
Wait until a specific state exists or has a specific value or set of values. The values can be specified as an inline dictionary as follows:
- wait until an entity exists - ``state: {entity: }``
- wait until an entity exists and has a specific value for its state: ``state: {entity: , value: {state: "on"}}``
- wait until an entity exists and has a specific value for an attribute: ``state: {entity: , value: {attributes: {attribute: value}}}``
Example to wait for an input boolean:
.. code:: yaml
state:
entity: input_boolean.appdaemon_enable # example entity name
value:
state: "on" # on needs to be in quotes
Example to wait for a light to be on full brightness:
.. code:: yaml
state:
entity: light.office_1 # example entity
value:
state: "on" # on needs to be in quotes
attributes:
brightness: 255 # full brightness
event
^^^^^
Wait for an event or an event with specific data
- wait for an event of a given type: ``{event_type: }``
- wait for an event with specific data: ``{event_type: , data: {service_data: {entity_id: }, service: }}``
Example to wait for ZWave to complete initialization upon a HASS restart:
.. code:: yaml
event:
event_type: zwave.network_ready
Example to wait for an input button before starting AppDaemon
.. code:: yaml
event:
event_type: call_service
data:
domain: input_button
service: press
service_data:
entity_id: input_button.start_appdaemon # example entity
API Usage
---------
Create apps using the `Hass` API by inheriting from the :py:class:`Hass ` class:
.. code:: python
from appdaemon.plugins.hass import Hass
class MyApp(Hass):
def initialize(self):
... # Your initialization code here
Read the `AppDaemon API Reference `__ to learn other inherited helper functions that
can be used by Hass applications.
Services
~~~~~~~~
Services are now called `actions` in Home Assistant, but are sometimes also referred to as `service actions`. Any of
them can be called by using the :py:meth:`call_service ` method with
their domain and service name.
The specific services available will vary depending on which integrations are installed in Home Assistant, but some
common ones would be ``light/toggle``, ``switch/turn_off``, etc. These services would control physical devices, but
services can do many other things as well.
.. admonition:: Service Name Delimiter
:class: note
AppDaemon uses the ``/`` delimter to separate the domain and service name, instead of the ``.`` used by Home
Assistant, so ``light.turn_on`` in Home Assistant becomes ``light/turn_on`` in AppDaemon.
Returning values
^^^^^^^^^^^^^^^^
As of AppDaemon v4.5.0, service calls can return values. When services are registered with AppDaemon, Home Assistant
indicates whether they return values and whether doing so is optional. AppDaemon uses that information to automatically
insert ``"return_response": true`` into the message it sends to Home Assistant if necessary.
.. admonition:: Home Assistant Responses
:class: note
Home Assistant always responds with some kind of acknowledgement, even for services that don't otherwise return a
value. AppDaemon includes whatever it gets from Home Assistant in the result dict.
.. list-table:: Result Dict
:header-rows: 1
:widths: 20 80
* - **Key**
- **Value**
* - ``id``
- Sequential ID of the websocket request. This matches the one that AppDaemon used with the initial request.
* - ``type``
- This will always be ``result``, as returned from Home Assistant.
* - ``success``
- Boolean representing whether the service call was successful or not.
* - ``result``
- Dict with the result of the service call if it was successful.
* - ``error``
- Dict with error information if the service call was not successful.
* - ``ad_status``
- Status from AppDaemon for the request.
* - ``ad_duration``
- Floating point number representing the round trip time of the request in seconds.
.. list-table:: AppDaemon Statuses
:header-rows: 1
:widths: 20 80
* - **Status**
- **Value**
* - ``OK``
- Inidcates that the process of calling the service didn't fail on the AppDaemon side. It could still have failed
on the Home Assistant side.
* - ``TIMEOUT``
- The service call timed out while waiting for a response from Home Assistant. This can happen if the
``ws_timeout`` is set too low or if Home Assistant is overloaded.
* - ``TERMINATING``
- Indicates that the task for the service call was cancelled while it was waiting for a response from Home
Assistant.
.. admonition:: Revealed Errors
:class: warning
With service calls now returning values, it's possible for operations that were silently failing before to now
produce warnings or errors. In most cases, this is beneficial/desired, but these can also be suppressed. For
example, Z-Wave devices are known to take a long time to respond, which can cause timeouts. However, most services
return nearly instantly.
Timeouts
^^^^^^^^
These timeouts determine how long AppDaemon will wait for a response from Home Assistant before giving up and returning
with a ``TIMEOUT`` for ``ad_status`` in the result dict.
.. list-table::
:header-rows: 1
:widths: 20 80
* - **Timeout**
- **Explanation**
* - ``hass_timeout``
- Provided with the service call and only affects that specific call.
* - ``ws_timeout``
- Provided in ``appdaemon.yaml`` and used as the default for all service calls. Can be overridden by
``hass_timeout``.
* - ``internal_function_timeout``
- Provided in ``appdaemon.yaml`` and controls how long the app will internally wait for a response from the main
AppDaemon thread.
Services and states
^^^^^^^^^^^^^^^^^^^
Setting the state of an entity only changes how it appears in Home Assistant, which is perfect for sensors, but not
devices like lights. To physically turn on a light, you should call the ``light/turn_on`` service. Merely setting the
state will not do that.
Service Registration
^^^^^^^^^^^^^^^^^^^^
The `Hass` plugin registers the initial set of services immediately after it authenticates with Home Assistant.
Afterwards, AppDaemon will register services whenever Home Assistant emits ``service_registered`` events. This makes all
of the services/actions in Home Assistant available to AppDaemon apps. Users can still register their own services from
apps using the ``register_service`` method.
Advanced Service Calls
^^^^^^^^^^^^^^^^^^^^^^
The `Hass` API ultimately wraps the
`calling a service action `_ feature
of the websocket API, so `anything` that can be done through that API, can be done with AppDaemon. Successfully calling
the service action merely depends on formatting the arguments to
:py:meth:`call_service ` correctly.
Here's one example
.. code-block:: python
from appdaemon.plugins.hass import Hass
class MyApp(Hass):
def initialize(self):
self.call_service(
"notify/alexa_media",
service_data={
"target": "media_player.tom_office",
"data": {"type": "announce"}
}
message="This is a test message"
)
Debugging
^^^^^^^^^
Some services require a complex set of data, which can require some tinkering to format correctly. There are a few
techniques that are useful for determining how that data should be formatted, starting with looking at the log text.
Unless suppressed, AppDaemon will log warnings for any failed service calls. These warnings will have whatever message
Home Assistant returned with the error, which is often helpful.
Beyond that, it's possible to inspect the services more closely by using the
:py:meth:`get_service_info ` method. This returns a nested dict
of information about the service, which comes from a call to
`get_services `_ that AppDaemon does
internally. This contains a little more detail than is visible in the Home Assistant developer tools.
.. code-block:: python
:emphasize-lines: 10
from appdaemon.plugins.hass import Hass
class MyApp(Hass):
def initialize(self):
service = "climate/set_temperature"
self.log_service_details(service)
def log_service_details(self, service: str) -> None:
if service_info := self.get_service_info(service):
self.log(f"{service}: {service_info['name']}")
for field, info in service_info['fields'].items():
self.log(f" {field}: {info['description']}")
match info:
case {'selector': {'number': {'min': min_value, 'max': max_value}}}:
self.log(f" {min_value} to {max_value}")
case {'selector': {'select': {'options': options}}}:
self.log(f" {', '.join(options)}")
case _:
pass
.. code:: text
INFO my_app: climate/set_temperature: Set target temperature
INFO my_app: temperature: The temperature setpoint.
INFO my_app: 0 to 250
INFO my_app: target_temp_high: The max temperature setpoint.
INFO my_app: 0 to 250
INFO my_app: target_temp_low: The min temperature setpoint.
INFO my_app: 0 to 250
INFO my_app: hvac_mode: HVAC operation mode.
INFO my_app: off, auto, cool, dry, fan_only, heat_cool, heat
Turning up the logging level to ``DEBUG`` to inspect the raw JSON being sent over the websocket.
Error Handling
^^^^^^^^^^^^^^
Python got a :py:ref:`match/case structure in v3.10 `, which has some very convenient patterns for handling the
results returned by service calls. See :py:ref:`this tutorial ` for more information about the different ways
to use it. For example, this service call will fail because it has ``bogus_arg=42``, which isn't allowed by the
``light/turn_on`` service in Home Assistant.
.. code-block:: python
from appdaemon.plugins.hass import Hass
from appdaemon.utils import format_timedelta
class MyApp(Hass):
def initialize(self):
service = "climate/set_temperature"
res = self.call_service(service, entity_id="climate.living_room", bogus_arg=42)
match res:
case {'success': True}:
self.log("Service call was successful")
case {'success': False, 'ad_status': 'OK', 'ad_duration': duration}:
time_str = format_timedelta(duration)
self.log(f"Service call failed on the Home Assistant side, and took {time_str}")
case _:
self.log(f"Unexpected response format from service call: {res}")
.. code:: text
WARNING HASS: Error with websocket result: invalid_format: must contain at least one of temperature, target_temp_high, target_temp_low.
INFO my_app: Service call failed on the Home Assistant side, and took 8.583ms
By default, AppDaemon will log warnings for any service call that fails. If you prefer to do error checking yourself on
a per-call basis you can set the ``suppress_log_messages`` argument to ``True`` when using
:py:meth:`call_service `, or you can suppress log messages globally by
setting ``suppress_log_messages`` to true in the plugin configuration.
.. code-block:: python
:emphasize-lines: 10
from appdaemon.plugins.hass import Hass
class MyApp(Hass):
def initialize(self):
service = "climate/set_temperature"
res = self.call_service(
service,
entity_id="light.kitchen",
bogus_arg=42,
suppress_log_messages=True,
)
match res:
case {'success': True}:
self.log("Service call was successful")
case {'success': False, 'ad_status': 'OK', 'error': error}:
self.handle_error(service, **error)
case _:
self.log(f"Unexpected response format from service call: {res}")
def handle_error(self, service: str, code: str, message: str) -> None:
self.log(f"Service call failed: {message}", level="ERROR")
match code:
case "invalid_format":
if service_info := self.get_service_info(service):
valid_fields = service_info.get("fields", {})
field_names = "\n".join(f" - {f}" for f in valid_fields)
self.log(
f"The service call had an invalid format. "
f"Valid fields are:\n{field_names}",
level="ERROR",
)
case _:
self.log(f"Unhandled error: {code}: {message}", level="ERROR")
.. code:: text
ERROR my_app: Service call failed: must contain at least one of temperature, target_temp_high, target_temp_low.
ERROR my_app: The service call had an invalid format. Valid fields are:
- temperature
- target_temp_high
- target_temp_low
- hvac_mode
Rendering Templates
~~~~~~~~~~~~~~~~~~~
Home Assistant has a powerful `templating `_ engine that
can be used to render templates in your apps. The `Hass` API provides access to this with the
:py:meth:`render_template ` method.
API Reference
-------------
.. autoclass:: appdaemon.plugins.hass.hassapi::Hass
:members:
:member-order: bysource