Slack Integration using Block Kit

Integrating with Slack and Block Kit

This mini-project is based on the following use-case:
Integration from Home Assistant sends a message to a slack channel with Yes/No options for the user to send a message back to Home Assistant to then be actioned.

This is based heavily on a post by SteveDinn

https://community.home-assistant.io/t/how-to-get-actionable-notifications-using-slack/145035

Sorry as a new user I cannot post clickable links.

however the modern way that Slack would like you to do this is with their Block Kit https://api.slack.com/block-kit.

General Requirements - Webhooks

This will need to use externally facing webhooks to receive messages in so ideally Nabu Casa makes this area work easily.

General Requirements - Slack App

You will need a Slack App that is correctly set up. The information provided by SteveDinn works well.

Configuration Files

Obviously how users split (or not) their configuration file can make a big difference to how to copy a project. I tend to use the format:

automation slack: !include_dir_merge_list ./automations/slack

The world slack after automations is only a name to help describe the group and means you can have other automation lines within you configuration.yaml file. This method then allows for separate automation files to be in the ./automations/slack folder.

Likewise I will have separate script files in a folder called scripts with their and then

script: !include_dir_merge_named ./scripts

Initial Post to Slack

This is the easiest part of the project.

This needs a 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: 'Bearer XXXInsert Token HereXXX'
payload: '{{  payload  }}'

This is used a general script to post a payload to an api end point. So we can set up a script below that takes a channel, question, yes_value and no_value. The yes_value and no_value will be returned to us after the button is clicked in Slack.

notify_slack_block:
  sequence:
    - service: rest_command.slack_api
      data_template:
        api: 'chat.postMessage'
        payload: >
          {
            "channel": "{{ channel }}",
            "blocks": 
              [
                {
                  "type": "section", 
                  "text": {"type": "plain_text", "text": "{{ question }}"}
                },
                {
                      "type": "actions",
                      "elements": [
                        {
                          "type": "button",
                          "text": {
                            "type": "plain_text",
                            "text": "Yes",
                            "emoji": true
                          },
                          "value": "{{ yes_value }}"
                        },
                        {
                          "type": "button",
                          "text": {
                            "type": "plain_text",
                            "text": "No",
                            "emoji": true
                          },
                          "value": "{{ no_value }}"
                        }
                      ]
                    }
              ],
            "text": "{{ question }}"
          }

As a test I created a custom button-card to send data to the script:

type: custom:button-card
color_type: card
color: rgb(223, 255, 97)
icon: mdi:slack
tap_action:
  action: call-service
  service: script.notify_slack_block
  service_data:
    channel: ChannelId
    question: Do you want to open door ?
    yes_value: yes_open_door
    no_value: no_open_door

This should appear in Slack:

Screenshot 2022-02-20 110110

Response from Slack

The documentation for Slack is pretty comprehensive and should be read at

https://api.slack.com/reference/interaction-payloads/block-actions

At a high level when a button is clicked the block_action payload is sent via a POST message to an endpoint (set up in the slack app) and this has information on what was clicked (we will get back the yes_value or no_value) and also a unique endpoint to acknowledge the button click.

Response Script

We will need a fairly basic response script to Slack. This does not need any authentication and the responseUrl is provided by Slack when they send back the button press.

This is used by Home Assistant to acknowledge that the button has been clicked and will actually replace the button with the text provided:

notify_slack_response:
  sequence:
    - service: rest_command.slack_api_response
      data_template:
        responseUrl: '{{ responseUrl }}'
        payload: >
          {
            "text": "{{ text }}"
          }

Home Assistant Webhooks

The setup for webhooks in Home Assistant can be a bit complicated. In a way you work backwards, create the trigger based on a webhook, this will then create the webhook, that can then be added to Nabu Casa and then finally added to Slack as the end point.

So create an automation:

alias: Slack Incoming
description: ''
trigger:
  - platform: webhook
    webhook_id: Slack_Incoming_Webhook
condition: []
action:
  - service: script.notify_slack_response
    data_template:
      responseUrl: >
        {%- set myPayload = trigger.data.payload | from_json -%} 
        {{ myPayload.response_url }}
      text: >
        {%- set myPayload = trigger.data.payload | from_json -%}
        {%- set ts_message = myPayload.message.ts | float -%} 
        {%- set ts_response = myPayload.actions[0].action_ts | float -%}
        {%- set ts_difference = ts_response - ts_message -%} 
        {% if ts_difference > 10 %}
            Message received {{ myPayload.actions[0].value }} {{ ts_difference |round|int }} seconds too late to be processed 
        {% else %}
            Message received {{ myPayload.actions[0].value }} {{ ts_difference |round|int }} seconds
        {% endif %}
  - service: script.process_slack_response
    data_template:
      response: >
        {%- set myPayload = trigger.data.payload | from_json -%}
        {%- set ts_message = myPayload.message.ts | float -%} 
        {%- set ts_response = myPayload.actions[0].action_ts | float -%}
        {%- set ts_difference = ts_response - ts_message -%}
        {% if ts_difference > 10 %}
            Too late 
        {% else %}
            {{ myPayload.actions[0].value }}
        {% endif %}
mode: single

Now under Configuration and Home Assistant Cloud, the new webhook will appear. This needs externally enabling via Nabu Casa and then you will get a external url like:

https://hooks.nabu.casa/XXXXtokenXXX

This needs to go into Slack. Under ‘Interactivity & Shortcuts’ in the ‘Settings’ of the app, add this to the Request Url.

So the flow is this:

  1. Home Assistant send interactive message to Slack (via a button or other automation)
  2. User click button
  3. Slack does HTTP POST to our webhook
  4. This is sent to Home Assistant and picked up by Automation above

The automation above is broken into two separate actions, one to acknowledge the request back to Slack and then to carry out any supporting actions. This is shown below is a simpler format

alias: Slack Incoming
description: ''
trigger:
  - platform: webhook
    webhook_id: Slack_Incoming_Webhook
condition: []
action:
  - service: script.notify_slack_response
    data_template:
      responseUrl: >
      text: >
  - service: script.process_slack_response
    data_template:
      response: >
mode: single

so for the response back to Slack we are sent URL to use in the payload (see example on the Slack Developers website). The response_url is provided and then passed back. Then there is a bit of calculation of the time difference between when the button was created to being pressed. This is set to 10 seconds to ensure that buttons are not clicked much late after the questions but can be changed to suit. Note that there is no other authentication so this might be something to consider, depending on the automations that are being used.

  responseUrl: >
    {%- set myPayload = trigger.data.payload | from_json -%} 
    {{ myPayload.response_url }}
  text: >
    {%- set myPayload = trigger.data.payload | from_json -%}
    {%- set ts_message = myPayload.message.ts | float -%} 
    {%- set ts_response = myPayload.actions[0].action_ts | float -%}
    {%- set ts_difference = ts_response - ts_message -%} 
    {% if ts_difference > 10 %}
        Message received {{ myPayload.actions[0].value }} {{ ts_difference |round|int }} seconds too late to be processed 
    {% else %}
        Message received {{ myPayload.actions[0].value }} {{ ts_difference |round|int }} seconds
    {% endif %}

The second part of the automation extracts out the value of the button passed in (our yes_value or no_value) and this is passed to script.process_slack_response. This separates any local actions to a different script.

  response: >
    {%- set myPayload = trigger.data.payload | from_json -%} {%- set
    ts_message = myPayload.message.ts | float -%}  {%- set ts_response =
    myPayload.actions[0].action_ts | float -%} {%- set ts_difference =
    ts_response - ts_message -%} {% if ts_difference > 10 %}
        Too late 
    {% else %}
        {{ myPayload.actions[0].value }}
    {% endif %}

Then finally the actual script for local actions. The choose option allows a if…then…else type structure. So here we are checking the response being passed in matches the action so no_open_door and yes_open_door.

For this example we are actually posting back to Slack to say that we are either opening the door or not (in reality you would have the actual local sequence to open to door) :

process_slack_response:
  sequence:
  - choose:
    - conditions: "{{ response == 'no_open_door' }}"
      sequence:
      - service: rest_command.slack_api
        data_template:
          api: 'chat.postMessage'
          payload: >
            {
              "channel": "C1U0VDLPM",
              "text": "No to opening the door"
            }
  - choose:
    - conditions: "{{ response == 'yes_open_door' }}"
      sequence:
      - service: rest_command.slack_api
        data_template:
          api: 'chat.postMessage'
          payload: >
            {
              "channel": "C1U0VDLPM",
              "text": "Yes to opening the door"
            }

Hi. Are you missing the code for the slack_api_response rest_command?

I was able to get it all working. Thanks for the post!!!

My code for slack_api_response:

slack_api_response:
    url: '{{ responseUrl }}'
    method: 'post'
    payload: '{{ payload }}' 

In my case, I wanted to replace the text of the original message when a button was clicked, so I am using “payload” here instead of just sending some text back to Slack. If you don’t want to send text, you truly only need to send a POST back to the response URL.

My script to call slack_api_response looks like this:

slack_notify_response:
  sequence:
    - service: rest_command.slack_api_response
      data_template:
        responseUrl: '{{ responseUrl }}'
        payload: >
          {
            "replace_original": "true",
            "blocks": 
              [
                { 
                  "type": "header",
                  "text": {
                    "type": "plain_text",
                    "text": "One or more garage doors are open!!",
                    "emoji": true
                  }
                },
                {
                  "type": "section", 
                  "text": {"type": "plain_text", "text": "{{ text }}"}
                }
              ]
          }

My automation webhook that calls slack_notify_response looks like this:

- alias: Slack Incoming - Garage Doors
  description: ''
  trigger:
    - platform: webhook
      webhook_id: Slack_Incoming_Webhook_Garage_Doors
  condition: []
  action:
    - service: script.slack_notify_response
      data_template:
        responseUrl: >
          {%- set myPayload = trigger.data.payload | from_json -%} 
          {{ myPayload.response_url }}
        text: >
          {%- set myPayload = trigger.data.payload | from_json -%}
          {%- set ts_message = myPayload.message.ts | float -%} 
          {%- set ts_response = myPayload.actions[0].action_ts | float -%}
          {%- set ts_difference = ts_response - ts_message -%} 
          {%- set message = myPayload.actions[0].value -%} 
          {% if ts_difference > 45 %}
              Message received {{ myPayload.actions[0].value }} {{ ts_difference |round|int }} seconds too late to be processed 
          {% else %}
              {% if message == 'close_main_garage' %}
                Closing main garage door
              {% elif message == 'close_extra_garage' %}
                Closing extra garage door.
              {% elif message == 'close_both_garage' %}
                Closing both garage doors.
              {% else %}
                Cancelled.
              {% endif %}
          {% endif %}
    - service: script.slack_process_garage_response
      data_template:
        response: >
          {%- set myPayload = trigger.data.payload | from_json -%}
          {%- set ts_message = myPayload.message.ts | float -%} 
          {%- set ts_response = myPayload.actions[0].action_ts | float -%}
          {%- set ts_difference = ts_response - ts_message -%}
          {% if ts_difference > 45 %}
              Too late 
          {% else %}
              {{ myPayload.actions[0].value }}
          {% endif %}
  mode: single

My slack_process_garage_response script is finally a script that closes the garage door(s) or does nothing, depending on the response sent to it.

Hope this helps others!!!