Skip to content

Instantly share code, notes, and snippets.

@costr
Last active January 8, 2020 14:55
Show Gist options
  • Save costr/1ad7ebc7fa0a78e34d1128cdf29e4fa7 to your computer and use it in GitHub Desktop.
Save costr/1ad7ebc7fa0a78e34d1128cdf29e4fa7 to your computer and use it in GitHub Desktop.
Slack Slash Command Issuing From Home Assistant

The Problem

I am in multiple Slack teams and have messages coming in frequently throughout the day. I am also on phone calls with clients all day who don't want to feel like I'm distracted during the call. I could put myself DND (do not disturb) on each slack instance but that's time consuming and I cannot always predict when I'll have a call.

My Current Solution

I found a brilliant Slack app, called "OTTT", that allows you to issue slash commands to set your DND for a specified amount of time. To make this more awesome it also allows you, for a modest service fee, to link your Slack team instances so that one slash command sets all of your workspaces to DND. [u]This is what I need![/u]

Now with use of the Ariela app call state sensor and the Slack notify component, with a few small tweaks, I can auto DND when I'm on a call....ah, sweet silence.

Install Instructions

  1. Create a folder called "slack" in your "custom_components" folder in Home Assistant directory.

NOTE: If this is your first custom component you may need to create it. It must be in the same folder that your configuration.yaml file exists

  1. Use the files from this gist and place them in individual files. Each file in the gist has the required file names (manifest.json, notify.py, __init__.py).

  2. The chat.command API, which does all the magic, isn't officially supported so we need a Legacy Token for this to work. Go to https://api.slack.com/custom-integrations/legacy-tokens, under "Legacy Token Generator" should be a table with your Workspace listed. Click the green button to "Create Token". Keep the page open to copy the token in a later step.

  3. Setting up the component configuration in Home Assistant, now, is the same as following the Home Assistant configuration steps https://www.home-assistant.io/integrations/slack.

NOTE: I repeat "configuration" a couple times there because you do not want to follow their "Setup" section instructions.

NOTE 2: This is also where you need to copy/paste your token from step 3.

  1. Restart Home Assistant.

  2. In Developer Tools -> Services type in notify.slack (or whatever you named it in your configuration) under "Service". If it shows up you're in business. If not try walking through the steps again, or post your error.

Extra credit: Since you're on the services page give it a test with a payload like "message: Hello there!" or "message: /shrug" you should see it show up in the default channel specified in the configuration setup.


Another way I use the Slack Slash Commands in my automations is through another app called “Picker”. This one is a bit of a shameless plug as it’s a slack app I actively work on but the app allows me to solve the problem of “who will do what and when?”

Chore Scenarios:

The laundry is done: html.notify (my family channel) /pick chore fold the laundry and the response @Costr you have been picked to complete the chore: fold the laundry... It’s trash day tomorrow /pick task take the trash to the curb and the response @Mom you have been picked to complete the task: take the trash to the curb... You could use this with other apps like /remind or get silly with /shrug if something happens


Also, if you go the same route and use OTTT this is my automation.

On Phone Pickup

- alias: OTTT On Call Status
  trigger:
  - entity_id: sensor.phonel_call_state_sensor
    platform: state
    to: offhook
  condition: []
  action:
  - data:
      message: /ottt 480 -q
      target:
      - '#home-automation'
    service: notify.slack
  initial_state: 'true'

Note: Notice the -q on the slash command, or message. This is to make the post silent so that you don’t add a line to your slack instance every time this runs. This is something I requested from the OTTT developer and he added in.

On Hangup

- id: '1554931223593'
  alias: OTTT Off Call Status
  trigger:
  - entity_id: sensor.phone_call_state_sensor
    from: offhook
    platform: state
    to: idle
  condition: []
  action:
  - data:
      message: /ottt end
      target:
      - '#home-automation'
    service: notify.slack
  initial_state: 'true'
"""The slack component."""
{
"domain": "slack",
"name": "Slack",
"documentation": "",
"dependencies": [],
"codeowners": [],
"requirements": []
}
"""
Slack platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.slack/
"""
import logging
import requests
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
import voluptuous as vol
from homeassistant.const import CONF_API_KEY, CONF_ICON, CONF_USERNAME
import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import (
ATTR_DATA, ATTR_TARGET, ATTR_TITLE, PLATFORM_SCHEMA,
BaseNotificationService)
REQUIREMENTS = ['slacker==0.12.0']
_LOGGER = logging.getLogger(__name__)
CONF_CHANNEL = 'default_channel'
CONF_TIMEOUT = 15
# Top level attributes in 'data'
ATTR_ATTACHMENTS = 'attachments'
ATTR_FILE = 'file'
# Attributes contained in file
ATTR_FILE_URL = 'url'
ATTR_FILE_PATH = 'path'
ATTR_FILE_USERNAME = 'username'
ATTR_FILE_PASSWORD = 'password'
ATTR_FILE_AUTH = 'auth'
# Any other value or absence of 'auth' lead to basic authentication being used
ATTR_FILE_AUTH_DIGEST = 'digest'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_CHANNEL): cv.string,
vol.Optional(CONF_ICON): cv.string,
vol.Optional(CONF_USERNAME): cv.string,
})
def get_service(hass, config, discovery_info=None):
"""Get the Slack notification service."""
import slacker
channel = config.get(CONF_CHANNEL)
api_key = config.get(CONF_API_KEY)
username = config.get(CONF_USERNAME)
icon = config.get(CONF_ICON)
try:
return SlackNotificationService(
channel, api_key, username, icon, hass.config.is_allowed_path)
except slacker.Error:
_LOGGER.exception("Authentication failed")
return None
class SlackNotificationService(BaseNotificationService):
"""Implement the notification service for Slack."""
def __init__(
self, default_channel, api_token, username, icon, is_allowed_path):
"""Initialize the service."""
from slacker import Slacker
self._default_channel = default_channel
self._api_token = api_token
self._username = username
self._icon = icon
if self._username or self._icon:
self._as_user = False
else:
self._as_user = True
self.is_allowed_path = is_allowed_path
self.slack = Slacker(self._api_token)
self.slack.auth.test()
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
import slacker
if kwargs.get(ATTR_TARGET) is None:
targets = [self._default_channel]
else:
targets = kwargs.get(ATTR_TARGET)
data = kwargs.get(ATTR_DATA)
attachments = data.get(ATTR_ATTACHMENTS) if data else None
file = data.get(ATTR_FILE) if data else None
title = kwargs.get(ATTR_TITLE)
isCommand = message[0] == '/'
for target in targets:
try:
if file is not None:
# Load from file or URL
file_as_bytes = self.load_file(
url=file.get(ATTR_FILE_URL),
local_path=file.get(ATTR_FILE_PATH),
username=file.get(ATTR_FILE_USERNAME),
password=file.get(ATTR_FILE_PASSWORD),
auth=file.get(ATTR_FILE_AUTH))
# Choose filename
if file.get(ATTR_FILE_URL):
filename = file.get(ATTR_FILE_URL)
else:
filename = file.get(ATTR_FILE_PATH)
# Prepare structure for Slack API
data = {
'content': None,
'filetype': None,
'filename': filename,
# If optional title is none use the filename
'title': title if title else filename,
'initial_comment': message,
'channels': target
}
# Post to slack
self.slack.files.post(
'files.upload', data=data,
files={'file': file_as_bytes})
elif isCommand:
if len(message.split(' ', 1)) > 1:
slackCommand = message.split(' ', 1)[0].strip() # Assume format of /{command} {command parameters}
commandParameters = message.split(' ', 1)[1]
else:
slackCommand = message
commandParameters = ' '
channel_id = self.slack.channels.get_channel_id(target.replace('#',''))
self.slack.chat.command(
channel_id,
slackCommand,
commandParameters)
else:
self.slack.chat.post_message(
target, message, as_user=self._as_user,
username=self._username, icon_emoji=self._icon,
attachments=attachments, link_names=True)
except slacker.Error as err:
_LOGGER.error("Could not send notification. Error: %s", err)
def load_file(self, url=None, local_path=None, username=None,
password=None, auth=None):
"""Load image/document/etc from a local path or URL."""
try:
if url:
# Check whether authentication parameters are provided
if username:
# Use digest or basic authentication
if ATTR_FILE_AUTH_DIGEST == auth:
auth_ = HTTPDigestAuth(username, password)
else:
auth_ = HTTPBasicAuth(username, password)
# Load file from URL with authentication
req = requests.get(url, auth=auth_, timeout=CONF_TIMEOUT)
else:
# Load file from URL without authentication
req = requests.get(url, timeout=CONF_TIMEOUT)
return req.content
if local_path:
# Check whether path is whitelisted in configuration.yaml
if self.is_allowed_path(local_path):
return open(local_path, 'rb')
_LOGGER.warning(
"'%s' is not secure to load data from!", local_path)
else:
_LOGGER.warning("Neither URL nor local path found in params!")
except OSError as error:
_LOGGER.error("Can't load from URL or local path: %s", error)
return None
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment