Triggering HomeAssistant Automations with Kapacitor

In an earlier post, I described how I've set up monitoring our home electricity usage using InfluxDB.

However, I thought it'd be good to be able to have this interact with our existing Home Automation stuff - we use HomeAssistant (previously Hass.io) for that.

In my earlier post, I described using Kapacitor to generate alert emails when the tumble dryer was finished, so in many ways it made sense to make this an extension of that. TICK scripts support calling arbitrary HTTP endpoints via the HTTPPost node, and HomeAssistant allows you to control sensors via HTTP API, so it's reasonably straightforward to implement.

Authorisation

HomeAssistant requires that API requests are accompanied by an API token, so the first thing to do is to generate one.

In HomeAssistant, you click your user icon and scroll down to Long-Lived Access tokens before clicking Create Token.

HomeAssistant Token Creation Section

Make a note of the token, it's needed later

 

The aim

Part of the point in having Kapacitor call HomeAssistant is so that you can base automations on a combination of Kapacitor's decisions and information that HomeAssistant holds.

So, for the initial project, I decided I wanted to test the following

  • Has the tumble dryer been turned on (Kapacitor knows/decides this)
  • Is it Sunny outside (HomeAssistant knows this)
  • If so, send a slightly grumbly notification

We need Kapacitor to push a boolean to HomeAssistant: is the tumble dryer on, or not?

HA Binary Sensor

Home Assistant's API allows us to control a Binary Sensor, which is perfect for this - we only care about whether it's on, or not.

To toggle the sensor, we just need to place a request to https://[home assistant]/api/states/binary_sensor.DEVICE_NAME with a JSON payload like the following

{"state": "on", "attributes": {"friendly_name": "Radio"}}

This brings us to our first issue - Kapacitor's HTTPPost node submits JSON, but it's not structured the way we need.

Luckily, Kapacitor allows us to specify a template to use in alerts, so in /etc/kapacitor/kapacitor.conf we add

[[httppost]]
    endpoint = "home_assistant_binary_sensor"
    url = "https://homeass.example.com/api/states/binary_sensor.{{.ID}}"
    alert-template = "{\"state\": \"{{.Details}}\", \"attributes\": {\"friendly_name\": \"{{.Message}}\"}}"
    headers = { Authorization = "Bearer REPLACE_THIS" }

Let's walk through this quickly:

  • endpoint specifies a name for the config - it allows us to have multiple httppost configs in the same config file. We'll reference the endpoint name from our TICK script.
  • url specifies where the POST should be made to, I've included a template string ({{.ID}}) so that we can control the device name from within the TICK script (that way we don't need multiple config sections if we want to add other sensors later)
  • alert-template is the magic we need, it provides a template for the request body - we're providing JSON structured the way HomeAssistant expects, and including template variables to be replaced by the TICK script.
  • headers adds our authorization header, you need to replace REPLACE_THIS with the long-lived access token generated earlier

We can then write a TICKscript to call this endpoint

var db = 'Systemstats'

var rp = 'autogen'

var measurement = 'power_watts'

var groupBy = []

var whereFilter = lambda: ("host" == 'tumble-dryer') AND isPresent("consumption")

var name = 'Tumble Dryer On'

var idVar = 'tumble_dryer_on'

var message = name

var idTag = 'alertID'

var levelTag = 'level'

var messageField = 'message'

var durationField = 'duration'

var outputDB = 'chronograf'

var outputRP = 'autogen'

var outputMeasurement = 'alerts'

var triggerType = 'threshold'

var details = '{{ if eq .Level "CRITICAL" }}on{{ else }}off{{ end }}'

var crit = 90

var data = stream
    |from()
        .database(db)
        .retentionPolicy(rp)
        .measurement(measurement)
        .groupBy(groupBy)
        .where(whereFilter)
    |eval(lambda: "consumption")
        .as('value')

var trigger = data
    |alert()
        .crit(lambda: "value" > crit)
        .message(message)
        .details(details)
        .id(idVar)
        .idTag(idTag)
        .levelTag(levelTag)
        .messageField(messageField)
        .durationField(durationField)
        .post()
        .endpoint('home_assistant_binary_sensor')

trigger
    |eval(lambda: float("value"))
        .as('value')
        .keep()
    |influxDBOut()
        .create()
        .database(outputDB)
        .retentionPolicy(outputRP)
        .measurement(outputMeasurement)
        .tag('alertName', name)
        .tag('triggerType', triggerType)

trigger
    |httpOut('output')

In my previous post, I built a slightly more complex set of criteria for when the check was critical, but for brevity have left that out here.

The bits we're really interested in for the purposes of this post are

var name = 'Tumble Dryer On'

var idVar = 'tumble_dryer_on'

var message = name

...

var details = '{{ if eq .Level "CRITICAL" }}on{{ else }}off{{ end }}'
....

var trigger = data
    |alert()
        .crit(lambda: "value" > crit)
        .message(message)
        .details(details)
        .id(idVar)
        .idTag(idTag)
        .levelTag(levelTag)
        .messageField(messageField)
        .durationField(durationField)
        .post()
        .endpoint('home_assistant_binary_sensor')

These set the variables we use, and call the HomeAssistant endpoint. If we assume the check's gone critical, we'd expect to see the following JSON payload being sent to https://homeass.example.com/api/states/binary_sensor.tumble_dryer_on

{"state": "on", "attributes": {"friendly_name": "Tumble Dryer On"}}

That POST creates a device within HomeAssistant, which we can then show the state of (screenshot is of the desk-plug I was initially testing with)

HomeAssistant state for desk usage high

 

Further Automation

We're now ready to create a HomeAssistant automation to trigger whenever our new device changes state:

- id: '1629723901331'
alias: Tumble dryer on
description: ''
trigger:
- platform: state
    entity_id: binary_sensor.tumble_dryer_on
    to: 'on'
condition:
- condition: state
    entity_id: weather.home
    state: sunny
action:
- service: notify.mobile_app
    data:
    message: Really? It's sunny...
mode: single

And voila! HomeAssistant will send a slightly passive-aggressive notification in the mobile app if the tumble dryer is turned on whilst it's Sunny outside. The wisdom of doing this (especially if you send the message to your spouse) is, of course, up for debate.

 

Value Sensor

Although it wasn't needed for this small project, having set up control of a binary sensor, I thought I'd quickly play around with how easy it would be to control a normal sensor (i.e. one that accepts values).

We have to use a different endpoint for this - https://[home assistant]/api/states/sensor.DEVICE_NAME

The JSON is more or less the same, though you can (optionally) include an attribute unit_of_measurement. I decided not to, as you can add the measurement on in HomeAssistant's gauges anyway, so it seemed better to have the config section be portable between TICK scripts.

We add a new section to /etc/kapacitor/kapacitor.conf

[[httppost]]
    endpoint = "home_assistant_sensor"
    url = "https://homeass.example.com/api/states/sensor.{{.ID}}_value"
    alert-template = "{\"state\": \"{{.Details}}\", \"attributes\": {\"friendly_name\": \"{{.Message}}\"}}"
    headers = { Authorization = "Bearer REPLACE_THIS" }

This time, when we want to throw an alert, we do things a little differently

var trigger = data
    |alert()
        .crit(lambda: "value" > crit)
        .message(message + '_value')
        .details('{{ index .Fields "value" }}')
        .id(idVar)
        .idTag(idTag)
        .levelTag(levelTag)
        .messageField(messageField)
        .durationField(durationField)
        .post()
        .endpoint('home_assistant_sensor')

The differences are

  • We've passed a template string into details() so that the value is embedded
  • We've called endpoint home_assitant_sensor instead
  • In both the Kapacitor config and the TICKscript, I've appended _value. This isn't required by HomeAssistant, it just means that if you're calling both endpoints in the same TICK script, the sensors are already differentiated so you can reuse variables

It's worth noting, too, that this quickly thrown together example will only trigger when the level crosses into, or leaves critical - you won't get realtime updates without adjusting the TICKscript.

With calls going out, you can then have HomeAssistant draw a gauge

Home Assistant usage gauge

Of course, you could already generate that gauge in Chronograf, but you can also now trigger HomeAssistant automations based on the level.

 

Conclusion

With a little bit of fiddling around, I've managed to get my TICK stack to call HomeAssistant when aspects of our electricity usage cross a threshold, enabling further automation against the devices that HomeAssistant has control of.

Aside from sending jokey notifications, I don't actually know what my use-case for this is quite yet. One obvious possibility is "skinflint mode" where if the day's cost crosses a threshold, Kapacitor tells HomeAssistant, which then turns all the lights off. I strongly suspect such an automation wouldn't be well received by others in the house though.

Slightly more seriously, though, I can see us reaching a point where I've the ability to monitor usage on certain "trouble" sockets - where I know things sometimes get forgotten/left on - and then have HomeAssistant turn them off if other criteria (time, proximity etc) are met.

Whilst it might seem, ostensibly, that HomeAssistant could handle that on it's own (if it can control the sockets, it can meter them), it's not always that straightforward. As noted in my previous post, our Tumble dryer takes rest-breaks, so additional averaging over time is required. It's all but certain that other use-cases will yield other examples of situations where the additional flexibility of being able to perform arbitrary processing is helpful.

Until then, though, I'll just enjoy the passive aggressive notification and await the day where HomeAssistant says it's sunny, but it's actually raining outside.