たるこすの日記

たるこすの日記

リアルからバーチャルへ、バーチャルからリアルへ

[おうちハック]Slack経由でGoogle Calendarの予定をRaspberry Piに音声通知させる

こんにちは、たるこすです。
前回の「「OK Google」音声コマンドを取得してTVの電源をON,OFFするAndroidアプリを作る」に引き続き、おうちハックネタです。

Raspberry PiGoogle Calendar の予定を音声でアナウンスさせたいと思います。 Raspberry Pi から直接 Google Calendar API を使って予定を取得してもいいのですが、実装の簡単さや今後の拡張性を考えて Slack を経由して行います。

Slack と Google Calendar を連携させ、予定の時間になったら Slack のチャンネルに通知が来るようにします。 そして、Raspberry Pi で Hubot を動作させ、メッセージに反応して音声で通知するようにします。

Slack と Google Calendar の連携

まず、Slack で音声通知用のチャンネルを作ります。今回は #notify という名前でチャンネルを作成しました。

次に、Slack の Apps & integrations の設定で、Google Calendar の連携を追加します。

その後、連携ルールの設定をします。 連携させたいカレンダーと通知先チャネルを選び、予定開始時の通知と一日の予定の通知を有効にします。

f:id:tarukosu:20161224093811p:plain

音声通知する Hubot の作成

次に Raspberry Pi で動作させる Hubot を作成します。

ひな形作成ツールのインストール

まずは、yo というひな形作成ツールと、hubot用のジェネレータをnpmを使ってインストールします。

$ npm install -g yo generator-hubot

nodejs と npm が入っていない場合には、先に以下の記事などを参考にインストールしてください。 Ubuntuに最新のNode.jsを難なくインストールする - Qiita

ひな形の作成

以下のコマンドを実行します。

$ mkdir my-hubot
$ cd my-hubot
$ yo hubot

作成者やボットの名前などを聞かれるので答えます。
Bot の名前は Deborah としました。
Bot adapter は slack としてください。

f:id:tarukosu:20161224140332p:plain

Hubot の動作確認

以下のコマンドで起動します。

$ bin/hubot

15行ほどのメッセージが表示された後、プロンプトが表示されます。 ボット名 ping を入力し、PONG が返ってくることを確認します。

Deborah> Deborah ping
Deborah> PONG

Slack と Hubot の連携

先ほど作成した Hubot を Slack と連携するには、Slack に Hubot 連携を追加して API Token を取得する必要があります。
API Token の取得は以下のように行います。

f:id:tarukosu:20161224093514p:plain f:id:tarukosu:20161224093516p:plain f:id:tarukosu:20161224093519p:plain

Token を取得したら、hubot 起動スクリプトを作成します。
ここでは start_hubot.sh という名前とします。
HUBOT_SLACK_TOKEN には、先ほど取得した API Token を設定します。

#!/bin/sh
export HUBOT_SLACK_TOKEN=xxxx-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxx
export PORT=8084
./bin/hubot --adapter slack

実行権限をつけて実行します。

$ chmod +x start_hubot.sh
$ ./start_hubot.sh

Hubot を slack のチャネルに追加するには、追加したいチャネルで以下のようにポストします。

/invite @[hubotの名前]

うまくいけば、以下のように "ボット名 ping" に対して、ボットが pong と返してくれるようなります。 f:id:tarukosu:20161224093523p:plain

hubot スクリプト作成

ここからは、メッセージを受け取って音声で通知するような Hubot のスクリプトを書いていきます。
Hubot のスクリプトは、coffeescript で書いたスクリプトを scripts フォルダ内に入れて起動すれば動作します。

ユーザからのメッセージを受け取る場合には、以下のようにrobot.hearを使えば良いです。

module.exports = (robot) -> 
  robot.hear /some message/i, (res) ->
    res.send "receiver your message"

しかし、今回は別のBotからのメッセージを受け取りたいので、この方法は使えません。
以下のように、adapter を使うことで bot からのメッセージも取得できるようになります。

module.exports = (robot) ->
  robot.adapter.client?.on? 'raw_message', (msg) ->
    msg = JSON.parse(msg)
    # write your code

Google Calendar Bot から送られてくるのは以下のようなメッセージです。

  • 1日の予定の通知
    • 予定がある場合、"There are 3 events today." のようなメッセージが送られてきて、予定の詳細は msg.attachments に格納されています。
    • 予定がない場合、"There is no events today." というメッセージが送られてきます。
  • 予定開始の通知
    • msg.attachments[0].pretext に "Event starting now:" というメッセージが入っていて、msg.attachments[0].more に予定詳細が入っています。

以下のコードを scripts/speak.coffee という名前で作成しました。

speak = (message, notify = true) ->
  console.log "speak #{message}"
  @exec = require('child_process').execSync
  if notify
    option = "-n"
  else
    option = ""

  command = "sh scripts/speak.sh #{option} '#{message}'"
  console.log "#{command}"
  @exec command, (error, stdout, stderr) ->
    console.log error if error?

module.exports = (robot) ->
  robot.adapter.client?.on? 'raw_message', (msg) ->
    msg = JSON.parse(msg)

    return if msg.type isnt 'message'
    if msg.subtype is 'bot_message'
      console.log "bot message"
      console.log msg

      if msg.text?
        match = msg.text.match(/There .* events? today/i)
        if match?
          speech_text = "きょうの予定は、" + msg.attachments.length + "件です"
          speak speech_text
          for attachment,index in msg.attachments
            speak attachment.title, false
        match = msg.text.match(/There is no events today/i)
        if match?
          speech_text = "きょうの予定は、ありません"
          speek speech_text
      else
        return unless msg.attachments
        attachment = msg.attachments[0]

        # google calendar event
        if attachment.pretext is "Event starting now:"
          speak attachment.more
          return

        # other message type 
        speak attachment.title
    else
      # user message
      return if msg.text is undefined
      speak msg.text
      return

音声通知用に speak メソッドを作成し、その中で speak.sh を呼ぶようにしています。
詳細は、下の「日本語文章の読み上げ」の項目を読んでください。

日本語文章の読み上げ

spaek.sh には、日本語文章の読み上げのためのスクリプトを書きます。
文字から音声に変換してくれるサービス(text2speech)はいろいろありますが、以前使ったことがあった VoiceText を使ってみます。
VoiceText Web API

API キーを取得すれば、シェルスクリプトから簡単にたたくことができるのでとても便利です。

また、いきなり音声通知が始まると初めのほうを聞き逃してしまうので、 飛行機でのアナウンス前に鳴る「ポーン」という効果音を鳴らしてから音声通知をするようにしています。

音源は音人さんのものを使わせていただきました。
効果音 旅客機内アナウンス(ポーン)<固定翼機<飛行<『 乗り物系音 』 by On-Jin ~音人~

plughw:3,0 の 3,0 の部分は使用するサウンドデバイスに合わせて変更してください。

#!/bin/sh
API_KEY="YOUR API KEY"
script_dir=$(cd $(dirname $0); pwd)

while getopts n OPT
do
    case $OPT in
    n) NOTIFY_SOUND=true
    esac
done

shift $((OPTIND - 1))

if [ $# -ne 1 ]; then
    echo "invalid argument"
    exit 1
fi

curl "https://api.voicetext.jp/v1/tts" \
     -u "$API_KEY:" \
     -d "text=$1" \
     -d "emotion=happiness" \
     -d "emotion_level=2" \
     -d "speaker=hikari" > tmp.wav

if [ $NOTIFY_SOUND ]; then
    mpg321 -a plughw:3,0 $script_dir/nori_ge_airplane_pon01.mp3 
    sleep 0.5
fi
aplay -D plughw:3,0 tmp.wav
rm tmp.wav

実行させた様子

実行させてみると、こんな感じです。


[Raspberry Pi] Google Calendar の予定をアナウンス

夜、給湯器の電源を切るのを忘れてしまうことがあるので、毎日繰り返す予定として Google Calendarに登録しています。

Slack は Google Calendar 以外にも多くのサービスと連携させることができるので、いろいろと応用ができそうです。