Skip to content

AppDaemon

Official Documentation

Installation

Don't make this harder than it needs to be, and just use Docker.

Workspace layout
./conf
├── appdaemon.yaml
├── apps
   ├── apps.yaml
   └── my_app.py
├── docker-compose.yml
├── requirements.txt
├── secrets.yaml
└── system_packages.txt

This service definition can also be added to other compose files

docker-compose.yml
services:
  appdaemon:
    container_name: appdaemon
    image: acockburn/appdaemon:dev # (1)!
    restart: unless-stopped # (2)!
    tty: true # (3)!
    volumes:
      - /etc/localtime:/etc/localtime:ro # (4)!
      - /etc/timezone:/etc/timezone:ro # (5)!
      - ./:/conf # (6)!
    ports:
      - 5050:5050
  1. Use acockburn/appdaemon:latest instead if anything gets weird with the dev version.
  2. A restart behavior is necessary for the container to get started when the host boots
  3. This is necessary to enable color output in the docker logs
  4. Passes the time of the host into the container for accurate logs
  5. Passes the timezone of the hose into the container for accurate logs
  6. Uses the directory of the docker-compose.yml file as the /conf folder.

It's much faster to install pre-built packages from the package manager rather than having them built during a pip installation

Find package names in the Alpine Package Lists. They're named a little differently than the ones used for apt/apt-get.

system_packages.txt
bash
cmake
git
tree

py3-pandas
py3-scipy
py3-h5py
py3-matplotlib
openblas
openblas-dev
requirements.txt
--extra-index-url https://www.piwheels.org/simple # (1)!
--only-binary=:all: # (2)!
rich
pvlib
matplotlib
  1. Sometimes adding other indexes can really help speed up installations.
  2. This syntax prevents pip from building the package, which can take a long time.
appdaemon.yaml
appdaemon:
  import_method: expert # (1)!
  latitude: 0
  longitude: 0
  elevation: 0
  time_zone: America/Chicago
  plugins: # (2)!
    HASS:
      type: hass
      ha_url: http://192.168.1.82:8123 # (3)!
      token: !secret long_lived_token # (4)!
    MQTT:
      type: mqtt
      namespace: default
      client_host: 192.168.1.221
      client_user: homeassistant
      client_password: !secret mqtt_password # (5)!
      client_topics:
        - zigbee2mqtt/#
http:
  url: http://0.0.0.0:5050 # (6)!
  1. This is necessary to be able to organize your code into Python packages
  2. There are basically 2 plugins - HASS and MQTT
  3. Need to be able to access Home Assistant at this address and port
  4. Secrets are loaded from secrets.yaml and accessed using !secret. The filename is plural, but the keyword is singular.
  5. Secrets are loaded from secrets.yaml and accessed using !secret. The filename is plural, but the keyword is singular.
  6. This has to match the right side of the port mapping in the docker-compose.yml file.
secrets.yaml
long_lived_token: <token> # (1)!
mqtt_password: <password> # (2)!
  1. Create this token from the security tab of your user profile in Home Assistant.
  2. MQTT password used when setting up the broker.
Docker Launch Mechanics

Uses a dockerStart.sh script as the ENTRYPOINT12, so this all gets run every time the container (re)starts.

  • Hard-codes the configuration directory as /conf
  • Copies over some sample folders and content if they don't exist
  • Modifies /conf/appdaemon.yaml with values from environment variables
  • Installs from system_packages.txt using an apk add command
  • Installs pip packages

    pip3 install --upgrade -r requirements.txt
    
  • Final command is

    exec python3 -m appdaemon -c $CONF "$@"
    

Docker Commands

All these commands have to be run from the same folder as the docker-compose.yml file.

The up command will download the images if necessary, create new containers, and start them.

docker compose up -d

The -d flag detaches the shell from the running container.

The down command will stop the containers and remove them.

docker compose down

These run commands start and run a one-off container with all the options defined in the compose file. The --rm flag makes the container remove itself after it stops.

docker compose run -it --rm appdaemon

This command is useful if you need to open a shell in the container. Overriding the entrypoint like that prevents the dockerStart.sh script from running too.

docker compose run -it --rm --entrypoint ash appdaemon

This attaches an ash shell to a running container named appdaemon (defined by the container_name).

docker exec -it appdaemon /bin/ash

App Structure

appdaemon.adapi.ADAPI

from appdaemon.adapi import ADAPI

class MyApp(ADAPI):
    def initialize(self):
        self.log('Initialized my app')

appdaemon.adbase.ADBase

from appdaemon.adbase import ADBase

class MyApp(ADBase):
    def initialize(self):
        self.adapi = self.get_ad_api()
        self.adapi.log('Initialized my app')

appdaemon.plugins.hass.hassapi.Hass

from appdaemon.plugins.hass.hassapi import Hass

class MyApp(Hass):
    def initialize(self):
        self.log('Initialized my app')

appdaemon.plugins.mqtt.mqttapi.Mqtt

from appdaemon.plugins.mqtt.mqttapi import Mqtt

class MyApp(Mqtt):
    def initialize(self):
        self.log('Initialized my app')
        name = self.args['button']
        self.listen_event(
            self.handle_button,
            'MQTT_MESSAGE',
            topic=f'zigbee2mqtt/{name}',
            namespace='mqtt',
            button=name,
        )

Callbacks

State Callbacks

Callback definition
def my_callback(self, entity: str, attribute: str, old: str, new: str, **kwargs):
    self.log('My callback')
Callback definition
def my_callback(self, entity: str, attribute: str, old: str, new: str, kwargs: dict):
    self.log('My callback')

Event Callbacks

Callback definition
def my_callback(self, event_name: str, data: dict, **kwargs: dict):
    self.log('My callback')
Callback definition
def my_callback(self, event_name: str, data: dict, cb_args: dict):
    self.log('My callback')

Scheduler Callbacks

Callback definition
def my_callback(self, cb_args: dict):
    self.log('My callback')
Callback definition
def my_callback(self, **kwargs: dict) -> None:
    self.log('My callback')

Example

from appdaemon.plugins.hass.hassapi import Hass

class MyClass(Hass):
    def initialize(self):
        self.run_in(
            self.my_callback,
            delay=1.5,
            title='Test title',
            message='Everything after delay is a custom keyword argument',
        )
        self.log('Running after a delay...')

    def my_callback(self, cb_args: dict) -> None:
        self.log(f'{cb_args["title"]}: {cb_args["message"]}')
from appdaemon.plugins.hass.hassapi import Hass

class MyClass(Hass):
    def initialize(self):
        self.run_in(
            self.my_callback,
            delay=1.5,
            title='Test title',
            message='Everything after delay is a custom keyword argument',
        )
        self.log('Running after a delay...')

    def my_callback(self, title: str, message: str, **kwargs) -> None:
        self.log(f'{title}: {message}')
        self.log(kwargs) # (1)!
  1. There's always a __thread_id key in the kwargs dict.

Entity Class

Accessed with self.get_entity(entity_id)