How to get actionable notifications using Slack

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?

  1. 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.
  2. It’s everywhere: Android, iOS, web, and native clients. No matter what platform you’re on, you can get push notifications.
  3. 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.

image

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:

  1. 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 :frowning:
  2. 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.

image
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”

29 Likes

I tried to install the Slack with your instructions but without success…
Can you give some more detailed instructions or some video tutorial :wink:
Thank you

Do your home assistant logs say anything? Otherwise, I have nowhere to start.

I configure Slack like in your instruction, place first code (rest_command…) in configuration.yaml, script code in script.ymal, !secret slackbot_token in secret.yaml…

But I nothing have of Slack in the log… :confused:

Ok. So let’s go through the components one by one and test them individually. Can you see your REST command as a service in the Dev Services page? Try calling it from there with appropriate parameters and see what happens.

I might be having the same issue as @marijandomi because I also don’t see much in the logs about it and it isn’t working.

I ran this sample curl command, which worked:

curl -X POST -H 'Authorization: Bearer ProperTokenHereBlahBlah' -H 'Content-type: application/json' --data '{"channel":"ProperChannelHereBlahBlah","text":"I hope the tour went well, Mr. Wonka."}' https://slack.com/api/chat.postMessage

I then tried testing the rest_command in the Services tab using the following, but it didn’t work:

rest_command:
  slack_api:
    url: https://slack.com/api/chat.postMessage
    verify_ssl: true
    method: 'post'
    content_type: 'application/json; charset=utf-8'
    headers:
      authorization: !secret slack_oauth_access_token
    payload: '{{ payload }}'
    timeout: 20

What I put in the Service Data area of the Service call to slack_api:

{
  "channel": "ProperChannelHereBlahBlah",
  "text": "This is the notification text"
}

The response from the logs:

2019-11-12 10:44:39 DEBUG (MainThread) [homeassistant.core] Bus:Handling <Event call_service[L]: domain=rest_command, service=slack_api2, service_data=channel=ProperChannelHereBlahBlah, text=This is the notification text>
2019-11-12 10:44:39 INFO (MainThread) [homeassistant.components.rest_command] Success call https://slack.com/api/chat.postMessage.

Am I missing anything obvious?

Edit: I don’t know how I missed that you tried curl, but I didn’t see it until after I made this post.

I hope I haven’t left something significant out of my steps. I think part of the problem may be that the REST Command doesn’t have great error reporting. Let’s try running this from the command line (I’m assuming you’re on some form of linux):

curl `
   -X POST `
   -H "Content-Type: application/json; charset=utf-8" `
   -H "Authorization: Bearer [slack-api-token]" `
   -d "{\"channel\":\"[channelId]\", \"text\":\"Test notification.\"}" `
   https://slack.com/api/chat.postMessage

Obviously, replace all the fields in square brackets [ ]. Hopefully that either works or gives you a useful error message.

One thing I saw in your REST command is that the “Authorization” header didn’t have the ‘A’ capitalized. I don’t know if it’s case sensitive or not. And since I can’t see your auth token, make sure the header starts with “Bearer”.

So if curl worked, the only difference I can see is the capitalization of “Authorization” in your REST Command. You have it capitalized in your curl command, but not in the REST Command YAML.

Edit: Capitalization is not the problem. tried lowercase in curl and it still works.

Have you left out “Bearer” from the secret that contains your slack token? Part of the problem is that if the call fails, the Slack API will still return success, but the result will contain JSON like this:

{"ok":false,"error":"not_authed"}

You never see that JSON with the REST Command.

I’ve tried both with and without the Bearer and get the same thing. I’ve also tried having “content_type” in the headers but no change.

To confirm, if you try testing it in the Services tab, does it work? And what do you put there?

Yeah, it works for me :expressionless:

My service YAML looks identical to how I showed it in the first post of the thread (including the “api” parameter). The parameters passed to it were the following:

api: chat.postMessage
payload: '{ "channel": "[my channel ID]", "text": "This is a test from dev-services." }'

Aha! I was missing the “payload” part beforehand.

payload: '{ [stuff] }'

Now the rest_command works from the Services tab. I’ll keep testing further.

Yeah, the payload wasn’t meant to be the parameters to the Home Assistant REST Command service. It’s meant to be the literal payload that gets sent to the Slack API.

This was part of the reason why I have the script that calls the REST Command for me.

Glad you got it working.

If it helps, I found and copied a workaround for having a secret in a template. You just need to create an input_text and populate it at startup:

input_text:
  slack_app_verification_token:
    initial: !secret slack_app_verification_token
    mode: password

Then the automation condition you had in your original post would look like this:

      - condition: template
        value_template: >
          {% set payload = trigger.data.payload | from_json %}
          {{ (payload.token == states('input_text.slack_app_verification_token')) and (payload.type == 'interactive_message') }}

I’m not sure of the security aspect, but it does work.

2 Likes

I’ve been trying to get this sorted without much luck.
I am getting stuck at the service call bit, I can’t seem to get a message out via hassio.
I have HTML5 and telegram messages working, and use duckdns https and nginx. I just cant seem to get this working which is a shame, because it seems to be the look I want in notifications.
The logs seems to be showing success, and if I try a message via slack test it will send a message to my slack channel.

from the dev services page I have tried various formats, including my channel id directly.
I have tried via nodered service call also and get similar results, e.g. a successful call out in the logs but no actual message in slack.
slack%20snip

2019-11-13 16:32:56 DEBUG (MainThread) [homeassistant.components.websocket_api.http.connection.1908785648] Received {'type': 'call_service', 'domain': 'rest_command', 'service': 'slack_api', 'service_data': {'api': 'chat.postMessage', 'payload': '{ "channel": "[!secret slack_ch]", "text": "This is a test from dev-services." }'}, 'id': 33}
2019-11-13 16:32:56 DEBUG (MainThread) [homeassistant.core] Bus:Handling <Event call_service[L]: domain=rest_command, service=slack_api, service_data=api=chat.postMessage, payload={ "channel": "[!secret slack_ch]", "text": "This is a test from dev-services." }>
2019-11-13 16:32:57 INFO (MainThread) [homeassistant.components.rest_command] Success call https://slack.com/api/chat.postMessage.
2019-11-13 16:32:57 DEBUG (MainThread) [homeassistant.components.websocket_api.http.connection.1908785648] Sending {'id': 33, 'type': 'result', 'success': True, 'result': None}
core-ssh:~#

I have this is my config

rest_command:
  slack_api:
    url: https://slack.com/api/chat.postMessage
    content_type: 'application/json; charset=utf-8'
    verify_ssl: true
    method: 'post'
    timeout: 20
    headers:
      Authorization: !secret slack_bot
    payload: '{{ payload }}'

I have tried it with url: https//slack.com/api/{{ api }}

I cant seem to get any joy.

The first issue I see here is that I don’t think you can use !secret values in the dev service tool. I’m not sure if that was your literal test, or you censored it for posting here, but for your test, try replacing that with the actual value and see if it works.

I don’t think the {{ api }} parameter bit has anything to do with the failure.

If you can get it working with curl like I described in this post, you should definitely be able to get it working using the REST Command service.

Good luck!

Edit: LInked directly to curl example post.

1 Like

Ok I think I got it. I was leaving the bearer out, and the square brackets in I think, definitely didn’t have bearer in my secret anyway. You are right about the !secret in call service too, no can do.

Thanks for your help!

Out of curiosity, any reason you didn’t go with blocks? It seems that is what Slack are moving towards.

HassOS SSH curl syntax for future peoples.

curl \
   -X POST \
   -H "Content-Type: application/json; charset=utf-8" \
   -H "Authorization: Bearer xoxb-789768131872-82fake472725-oc1RQADHSHEHUoNBUEYtLlK" \
   -d "{\"channel\":\"CPfakeTXC\", \"text\":\"Test notification.\"}" \
   https://slack.com/api/chat.postMessage

I hope that’s not your actual slack token.

nah its fake one.

Ahh they are quite similar looking to telegram messages. Did you manage to get them display the buttons when the notifications arrive, as in, so you don’t have to go into the app to see the buttons?

Yeah, they are similar to telegram…that’s what I was using before Slack. It is a bummer that nothing seems to display the actions on the actual notification, but I haven’t found anything that does this.

Still Slack ticked a lot of boxes for me that Telegram didn’t, so I moved over.

Great guide! Going through the REST API directly rather than this https://www.home-assistant.io/integrations/slack/ is much more straightforward and flexible