How to get notifications working using Slack
And how to perform actions in Home Assistant based on those notifications
Why Slack?
There are tons of ways to send out notifications from Home Assistant. Why is Slack now my preferred method?
- Because they have a free tier of service. Under their free plan, you get just about all the features of a paid plan, but you can only see your last 10,000 messages. That’s good enough for me. Most of my notifications are not useful past a day anyway.
- It’s everywhere: Android, iOS, web, and native clients. No matter what platform you’re on, you can get push notifications.
- Channels. You can create as many different channels as you like, both public and private. Then you can divide up your notifications into different channels. People who care about certain types of notifications can join that particular channel and get notified for them. If you don’t want certain types, leave the channel, or turn off notifications for that channel. It leaves the decision completely in the hands of the user and doesn’t have to be taken into account by Home Assistant.
Create a Slack organization
Head over to http://slack.com and create a team. Call it whatever you want, but it has to be a globally unique name. You’ll use this name later to log in (https://.slack.com). Add yourself and whomever else you would like to your team.
Create a Slack app for your organization
The next step is to create an application that has access to your organization. Go to https://api.slack.com/apps. If you’re logged into your org, you should now see the “Create New App” button in the upper right section of the main panel. Give your app a name and select your workspace. I recommend something like “Home Assistant” or “Notifications”.
In the OAuth & Permissions section, grant your app the “chat:write:bot” scope. This permission will allow the app to send messages as your app’s name.
Then back up at the top of the page, click “Install App to Workspace”. Review the permissions and click ‘Allow’. Copy that OAuth Access Token and keep it safe. This is something you’re going to want to have in your “secrets.yaml” file.
Configure Home Assistant
We’re going to need a way to call into Slack to fire off our notifications. I tried using the notify.rest
component, but it wasn’t flexible enough for what I was doing. I ended using a rest_command
. Maybe somebody can get it to work with a bit more patience. Besides, you may eventually want to use more of the Slack API than just posting messages:
rest_command:
slack_api:
url: https://slack.com/api/{{ api }}
content_type: 'application/json; charset=utf-8'
verify_ssl: true
method: 'post'
timeout: 20
headers:
Authorization: !secret slackbot_token ←- this is "Bearer [your OAuth token]"
payload: '{{ payload }}'
I also set up this script to make it a bit easier to use:
script:
notify_slack:
sequence:
- service: rest_command.slack_api
data_template:
api: 'chat.postMessage'
payload: >
{
"channel": "{{ channel }}",
"text": "{{ message }}",
"attachments": {{ attachments if (attachments is defined) else '[]' }}
}
When you want to send an automation in a script or automation, you’ll need to know the ID of the Slack channel you want to post to (How do I find the ID of a Slack channel?). You can call it like this:
- service: script.notify_slack
data_template:
channel: !secret.slack_channel_id
message: 'This is the notification text'
You’ll notice I left the attachments
parameter unset. We’ll get to that.
We now have notifications working!
What about actionable notifications?
Well, yeah, actionable notifications. You can get some options in your notification and click on one to perform an action with context. First, we’ll need to add some options to our notification.
Adding actions to your notifications
Actions are things you want to present to the notify-ee that they can choose to do. An actionable notification that I find really useful is one that is sent when my garage door is left open late at night.
- service: script.notify_slack
data_template:
channel: !secret.slack_channel_id
message: "The Garage door is open and it’s late."
attachments:
- title: Close the Garage door?
callback_id: cover_left_open_callback
color: '#03a9f4'
attachment_type: default
actions:
- text: Close it
value: Close it
name: 'cover.garagedoor'
type: button
- text: Leave it
value: Leave it
name: 'anything_just_not_blank'
type: button
The result of this action is pictured above.
So what happens when you click one of those buttons?
Set up a callback to perform actions
On https://api.slack.com, when looking at your app, click on “Interactive components” and turn on “Interactivity”. In the “Request URL” box, you’re going to enter the address of a webhook that you’ll create in Home Assistant (something like https://[your domain]/api/webhook/[webhook_id]). When you click one of those buttons in your notifications, Slack will POST a bunch of JSON to that URL (see the bottom of this post for an example of what Slack sends to this webhook). We can set up an automation to deal with that.
This section requires at least v0.101 of Home Assistant because it uses the new to_json
and from_json
filters.
Slack’s callbacks are kind of weird. They contain an x-form-urlencoded
body that has a parameter called payload
which contains a JSON formatted string. Here’s where the new filters come in. We can choose whether or not to use trigger.data
or trigger.json
, but you can’t easily deal with a message body that contains bits of both. (In a pinch, you could have one webhook call into another webhook, but only pass {{ trigger.data.payload }}
as the payload for the next webhook. Then the second webhook would interpret the JSON for you.) Using the from_json
filter, we can just decode it.
In this webhook-triggered automation, we’re going to have a condition that checks two things:
- The payload contains your app’s verification token. You can find this on your app’s “Basic Information” page. Ordinarily, I’d put this sort of value in a
!secret
, but I couldn’t figure out how to use a secret in a template - That the type of the payload is ‘interactive_message’
automation:
- alias: slack_webhook
trigger:
- platform: webhook
webhook_id: <webhook ID>
condition:
- condition: template
value_template: >
{% set payload = trigger.data.payload | from_json %}
{{ (payload.token == <slack app verification token>) and (payload.type == 'interactive_message') }}
action:
- service_template: >
{%- set payload = trigger.data.payload | from_json -%}
script.{{ payload.callback_id }}
data_template:
payload: '{{ trigger.data.payload }}'
In the example of the notification with buttons, I specified the callback_id
to be cover_left_open_callback
, so this automation will end up calling script.cover_left_open_callback
with the entire Slack payload as a parameter. Here’s what the cover_left_open_callback
script looks like:
script:
cover_left_open_callback:
sequence:
- service: script.callback_handled
data_template:
replace_original: true
payload: '{{ payload }}'
- condition: template
value_template: >
{%- set action = (payload | from_json).actions[0] -%}
{{ (action.value == "Close it") and not (action.name == "")}}
- service: cover.close_cover
data_template:
entity_id: >
{%- set entity_id = (payload | from_json).actions[0].name -%}
cover.{{ entity_id if (not entity_id.startswith("cover.")) else entity[6:] }}
In my example, the value
of the action is used to identify that action in the callback and the name
is used to identify the entity_id
of the cover I want to close. These fields are totally up to you how you use them – just make sure that the actions match the callback script. In another one of my automations, I even put a JSON encoded string into the name
field, and pass multiple values.
The final piece of the puzzle is to let the user on the notification end know that something has happened. The callback_handled
script will do that for us. It’ll replace the actions of the original message with some text that says: “Handled with ‘[action]’. by @[user]” so that if this notification is in a public channel where other people can see it, they’ll also see that something was done about it, and who did it.
Here’s what the script (and associated rest_command
) looks like:
rest_command:
slack_response:
url: '{{ response_url }}'
content_type: 'application/json; charset-utf-8'
verify_ssl: true
method: 'post'
timeout: 20
payload: >
{
"replace_original": {{ replace_original if (replace_original is defined) else 'true' }},
"text": "{{ message }}",
"attachments": {{ attachments if (attachments is defined) else '[]' }}
}
script:
callback_handled:
sequence:
- service: rest_command.slack_response
data_template:
response_url: >
{%- set pl = payload | from_json -%}
{{ pl.response_url }}
replace_original: >
{{ replace_original if (replace_original is defined) else 'true' }}
attachments:
- pretext: >
{%- set pl = payload | from_json -%}
{{ pl.original_message.text }}
title: >
{%- set pl = payload | from_json -%}
Handled with '{{ pl.actions[0].value }}' by <@{{ pl.user.id }}>
color: >
{%- set color = (payload | from_json).original_message.attachments[0].color -%}
{{ "" if color.startswith("#") else "#" }}{{ color }}
Summary
Hopefully this will be useful for people. Although it’s pretty complicated once you get into the callbacks and stuff, I think it’s something you can follow along with. It was definitely fun to work on in the first place.
Before I was sending out multiple notifications for each event for each person who wanted to know about it, but now I can just post any particular type of notification to a particular channel in my Slack team, and users can choose which channels they want to join and which ones they want to get notifications for.
Slack’s API is so rich and deep, you can go way further than I have if you care to. I’m interested to see any further applications of this in other automations.
Appendix 1: The JSON from a Slack callback payload
{
"type": "interactive_message",
"actions": [
{
"name": "cover.garagedoor",
"type": "button",
"value": "Close it"
}
],
"callback_id": "cover_left_open_callback",
"team": {
"id": "<Slack team ID>",
"domain": "<Slack team name>"
},
"channel": {
"id": "<Slack channel ID>",
"name": "<Slack channel name>"
},
"user": {
"id": "<Slack user ID>",
"name": "<Slack user name>"
},
"action_ts": "<action timestamp",
"message_ts": "<message timestamp",
"attachment_id": "1",
"token": "<slack verification token>",
"is_app_unfurl": false,
"original_message": {
"type": "message",
"subtype": "bot_message",
"text": "The Garage door is open and it's late",
"ts": "<message timestamp>",
"bot_id": "<bot ID>",
"attachments": [
{
"callback_id": "cover_left_open_callback",
"title": "Close the Garage door?",
"id": 1,
"actions": [
{
"id": "1",
"text": "Close it",
"value": "Close it",
"name": "cover.garagedoor",
"type": "button"
},
{
"id": "2",
"text": "Leave it",
"value": "Leave it",
"name": "dismiss",
"type": "button"
}
]
}
]
},
"response_url": "<response URL>",
"trigger_id": "<trigger ID>"
}
Edit: Moved this to “Share your Projects”