Automations – The programmatic approach

My Goal

I wanted to make a Home Assistant automation to watch the exchange rate of bitcoins. I wanted it to be shown in HA like this:

It should have these features:

  1. The warning levels can be set from two input sliders.
  2. If the exchange rate falls under the “low warning” level, I want to get a notification every 5 hours with the current rate.
  3. If the exchange rate goes over the “high warning” level, I want to get a notification every 5 hours with the current rate.
  4. When it comes back to the normal range i want to get notified. As long as it stays here, I don’t want any more notifications.

All this could probably be made with normal automations, but it would look rather ugly, so I decided to do it in pure python code.

 

Appdaemon

I found the great appdaemon project that allowed me to do automations in HA programmaticaly. It hooks into the eventbus of HA and allows me to listen to any events and states in HA. NEAT!

First I installed it in virtual env because I dont like running things as root:

sudo su -s /bin/bash homeassistant
source /srv/homeassistant/bin/activate
pip3 install appdaemon

Then i wanted it to start up at boot time by creating /etc/systemd/system/multi-user.target.wants/appdaemon.service:

[Unit]
Description=AppDaemon
After=homeassistant.service
Requires=homeassistant.service

[Service]
Type=simple
User=homeassistant
ExecStart=/srv/homeassistant/bin/appdaemon -c /home/homeassistant/appdaemon/conf

[Install]
WantedBy=multi-user.target

And then enabled and started it:

systemctl enable appdaemon.service
systemctl start appdaemon.service

Appdaemon expects a configuration file in the installation directory. In my case /home/homeassistant/appdaemon/conf/appdaemon.yaml. This file has to be pointing to my HA:

AppDaemon:
  logfile: STDOUT
  errorfile: STDERR
  threads: 10
HASS:
  ha_url: http://127.0.0.1:8123

 

My bitcoin class setup

First I needed to tell appdaemon that I have some code for it. It is done by adding this to the conf/appdaemon.yaml file:

BitcoinWatch:
  module: bitcoinwatch
  class: BitcoinWatch
  low_slider: input_slider.btc_low
  high_slider: input_slider.btc_high
  notify_who: pushover
  warning_frequency: 300 # every 5 hours if outside limit

Line 2 tells appdaemon to load a file named “bitcoinwatch.py”

Line 3 tells appdaemon which classname inside that file to instanciate.

Line 4-7 are configuration parameters that I want to pass to my class. That means that I can configure my class without changing the actual code.

The cool thing about appdaemon is that it constantly scans this file for changes. As soon as you change it, appdaemon reloads it.

 

The actual bitcoin class

In the installation folder of appdaemon I created a file conf/apps/bitcoinwatch.py :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import appdaemon.appapi as appapi
import time

class BitcoinWatch(appapi.AppDaemon):

    has_warned=False
    last_warned_time=time.time()

    def initialize(self):
        self.btc_low=self.get_state( self.args["low_slider"] )
        self.btc_high=self.get_state( self.args["high_slider"] )
        self.log("Bitcoin is now watching exchange rate between {}$ and {}$".format(self.btc_low, self.btc_high))

        self.listen_state(self.btc_low_slider_changed, self.args["low_slider"])
        self.listen_state(self.btc_high_slider_changed, self.args["high_slider"])
        self.listen_state(self.exchange_rate_changed, "sensor.exchange_rate_1_btc")

    def btc_low_slider_changed(self, entity, attribute, old, new_slider_value, kwargs):
        self.btc_low=new_slider_value
        self.log("Bitcoin Low Warning Limit Changed to: {}$".format(self.btc_low))

    def btc_high_slider_changed(self, entity, attribute, old, new_slider_value, kwargs):
        self.btc_high=new_slider_value
        self.log("Bitcoin High Warning Limit Changed to: {}$".format(self.btc_high))

    def exchange_rate_changed(self, entity, attribute, old, new_rate, kwargs):
        if self.has_warned == True:
            if time.time()-self.last_warned_time > self.args["warning_frequency"]*60:
                self.log("It has now been long time since we last warned. We can do it again.")
                self.last_warned_time=time.time()
                self.has_warned=False
            if new_rate < self.btc_high and new_rate > self.btc_low:
                msg="Bitcoinrate is now normalized again and is between {}$ and {}$".format(self.btc_low, self.btc_high)
                self.send_notification_msg(msg)
                self.has_warned=False
        if self.has_warned == False:
            if new_rate > self.btc_high:
                msg="Bitcoinrate is higher than {}$ It is {}".format(self.btc_high, new_rate)
                self.send_notification_msg(msg)
                self.has_warned=True
            if new_rate < self.btc_low:
                msg="Bitcoinrate is lower than {}$ It is {}".format(self.btc_low, new_rate)
                self.send_notification_msg(msg)
                self.has_warned=True

    def send_notification_msg(self, msg):
        self.notify(msg, name=self.args["notify_who"],title="Bitcoin watch")
        self.last_warned_time=time.time()
        self.log(msg)

Line:

9: This method gets called when appdaemon loads the class.

10-11: Get the actual name of my two sliders from the appdaemon.yaml config file.

14-16: I register eventhandlers for the events when the input sliders change or the exchange rate changes.

18-24 updates internal values for low and high warnings whenever they are change in the HA user interface.

26-44 handles all the logic of when to warn depending on the exchange rate. This should be self explanatory.

46-49 Is the function called when a warning should be sent to me. It pulls the name of the service to notify (I have setup pushover in HA’s configuration.yaml to my phone)

Again appdaemon is very cool – as soon as you do any written changes to the code, it automatically reloads the class. This gives very fast feedback of the development process.

 

My HA configuration.

For completeness I here provide the necessary configuration that should be added to Home Assistant to make all this work.

notify:
  - name: pushover
    platform: pushover
    api_key: !secret pushover_api_key
    user_key: !secret pushover_user_key

sensor:
  - platform: bitcoin
    display_options:
      - exchangerate
      - trade_volume_btc

input_slider:
  btc_low:
    name: Bitcoin Low Warning
    min: 1000
    max: 4000
    step: 10
  btc_high:
    name: Bitcoin High Warning
    min: 1000
    max: 4000
    step: 10

group:
  BTC:
    view: false
    entities:
      - sensor.exchange_rate_1_btc
      - input_slider.btc_low
      - input_slider.btc_high
    name: Bitcoins

 

Debugging

When changing code I was having a session open watching the following command:

journalctl -u appdaemon.service -f

It shows a very valuable output of the log commands in the class and python error messages. It looks like this:

Jul 22 08:33:26 homeauto appdaemon[18042]: 2017-07-22 08:33:26.674121 INFO Got initial state
Jul 22 08:33:26 homeauto appdaemon[18042]: 2017-07-22 08:33:26.676985 INFO Loading Module: /home/homeassistant/appdaemon/conf/apps/bitcoinwatch.py
Jul 22 08:33:26 homeauto appdaemon[18042]: 2017-07-22 08:33:26.678677 INFO Loading Object BitcoinWatch using class BitcoinWatch from module bitcoinwatch
Jul 22 08:33:26 homeauto appdaemon[18042]: 2017-07-22 08:33:26.704705 INFO BitcoinWatch: Bitcoin is now watching exchange rate between 2510.0$ and 3000.0$
Jul 22 08:33:26 homeauto appdaemon[18042]: 2017-07-22 08:33:26.705230 INFO App initialization complete
Jul 22 08:33:26 homeauto appdaemon[18042]: 2017-07-22 08:33:26.705669 INFO Dashboards are disabled
Jul 22 08:33:26 homeauto appdaemon[18042]: 2017-07-22 08:33:26.710380 INFO Connected to Home Assistant 0.49.0
Jul 22 08:59:00 homeauto appdaemon[18042]: 2017-07-22 08:59:00.716227 INFO BitcoinWatch: Bitcoin Low Warning Limit Changed to: 2940.0$
Jul 22 08:59:02 homeauto appdaemon[18042]: 2017-07-22 08:59:02.073235 INFO BitcoinWatch: Bitcoin High Warning Limit Changed to: 3290.0$
Jul 22 09:03:19 homeauto appdaemon[18042]: 2017-07-22 09:03:19.613433 INFO BitcoinWatch: Bitcoinrate is lower than 2940.0$ It is 2752.18
Jul 22 09:03:41 homeauto appdaemon[18042]: 2017-07-22 09:03:41.221077 INFO BitcoinWatch: Bitcoin Low Warning Limit Changed to: 2310.0$
Jul 22 09:08:19 homeauto appdaemon[18042]: 2017-07-22 09:08:19.621481 INFO BitcoinWatch: Bitcoinrate is now normalized again and is between 2310.0$ and 3290.0$

 

Whats next?

I hope this can get you started writing more advanced automation. The yaml of HA can easily get ugly and out of hand in advanced automations. This is an easy way to avoid that.

Commens are welcome 🙂

4 thoughts to “Automations – The programmatic approach”

  1. sudo su -s /bin/bash homeassistant
    source /srv/homeassistant/bin/activate
    sudo pip3 install appdaemon

    You probably didn’t mean ‘sudo pip3’ as this would put it out of the virtualenv. homeassistant user doesn’t normally have sudo access anyway.

  2. Can you please explain where does the “new_slider_value” argument come from? From the documentation i can only see that the typical arguments are self, entity, attribute, old, new, and kwargs. I am still struggling on understanding the syntax and structure.

Leave a Reply

Your email address will not be published. Required fields are marked *