SORACOM Enterprise Buttonで寝過ごしを防ぐ(Google Cloud Functions/Slackリマインダー通知版)

<img width="450" alt="train_button.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/c72c8cee-bd75-becb-91e5-10ab1fb77c9d.png">

この記事は[SORACOM Advent Calendar 2019](https://qiita.com/advent-calendar/2019/soracom) 4日目の記事になります。

# TL;DR(まとめ)

* SORACOM LTE-M Button for Enterpriseを使用、簡易位置情報から近隣の駅を把握、目標駅までの時間を考慮してSlackにリマインドする仕組みを構築した
* 駅の位置とリマインド時間対応表を用意しておき、電車に乗った時にボタンを押しておくことで、時間を考慮してリマインドしてくれる
* 安心して電車で眠れますね、というのは違うと思う笑
 

# はじめに

忘年会シーズンということで、飲酒して帰りの電車に乗ることが増えてきます。
寝過ごし防止のためにも、降りる駅近くになったら通知してほしいですよね。

そのためには今自分が居る位置を大まかにも把握する必要がありますが、
幸いにも[SORACOM LTE-M Button for Enterprise](https://soracom.jp/products/gadgets/enterprise_button/)(通称　しろボタン)で基地局の緯度/経度情報を取得することができます。

2019年7月のSORACOM Discoveryで発表されたSORACOM Funkと併せて作ります。

※嬉しいことにDiscovery後のナイトイベントでクイズの景品としてしろボタンをゲットできたこともあり、デバイス選定は悩みませんでしたｗ

では「帰りの電車の乗車中にしろボタンを押しておけば、しろボタンを押した場所に応じて、最寄り駅近くでSlackに通知が来る仕組み」を作りましょう。
これで寝過ごしは（きっと）防げますね！

リマインドイメージです。このリマインドが降車時間近くになったら自分に通知されます。
<img width="653" alt="Slack_reminder.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/ddeef9da-3e3c-591a-3f17-9b06705db4d6.png">
Slackにはリマインド作成のためのAPIが用意されています。[Slackのドキュメントはこちら](https://api.slack.com/methods/reminders.add)

事前にリマインドを欲しい駅とそこに至るまでの各駅からの時間を把握しておきます。
小竹向原駅に着くころにリマインドして欲しい有楽町線のユーザーなので、以下のようになります。

| 駅名 | 駅の(緯度,経度)  | リマインドまでの時間 |
|:------:|:------:|:------:|
| 小竹向原駅| (35.7433411,139.6773321)  | in 5 seconds |
| 千川駅| (35.7382658,139.68716)  | in 30 seconds |
| 要町駅| (35.7331756,139.6969832)  | in 1 min |
| 池袋駅| (35.7295028,139.7087114)  | in 3 min |
| 飯田橋駅| (35.7020837,139.7428291)  | in 14 min |
| 永田町駅| (35.6786011,139.738092)  | in 20 min |
| 有楽町駅| (35.6749187,139.7606258)  | in 24 min |
| 豊洲駅| (35.654825, 139.796324)  | in 32 min |
| 新木場駅| (35.6459065,139.8245976)  | in 37 min |

※駅名と位置を中略しています。
※時間はin XX minと書くことで、だいたいXX分後にリマインドがきます。だいたい、というのがポイントです。小竹向原駅近くに居る場合はすぐ（5sec）通知しています。

# 構築内容
以下の順次で作っていきます。[]は設定対象です。
※しろボタンの初期設定は終わっているものとします。

1. Google Cloud Functions上に関数を作成する[Cloud Functions]
2. 簡易位置情報をGoogle Cloud Functions宛に送るよう設定する[SORACOM Console]
3. 所要時間後にリマインドするようSlackのキー取得する[Slack API]
4. SlackのキーをCloudFunctionsに登録する[Cloud Functions]

# 構築手順

## Google Cloud Functions上に関数を作成する
任意のGCPプロジェクトを作成し、Cloud Functionsで以下Pythonコードを作成します。

[こちらの記事](https://qiita.com/sousoumt/items/8c7d28c42f8a47c41831#cloudfunctions%E8%A8%AD%E5%AE%9A)を参考に行い、CloudFunctionsの中身を以下にしましょう。

```python:CloudFunctions(main.py)
import jwt
import os
import json
import urllib.request
import math

# Set stations as you want
stations = [
    {'name': '小竹向原', 'position': (35.7433411,139.6773321), 'remind_time':'in 5 seconds'},
    {'name': '千川', 'position': (35.7382658,139.68716), 'remind_time':'in 30 seconds'},
    {'name': '要町', 'position': (35.7331756,139.6969832), 'remind_time':'in 1 min'},
    {'name': '池袋', 'position': (35.7295028,139.7087114), 'remind_time':'in 3 min'},
    {'name': '東池袋', 'position': (35.7260176,139.7168035), 'remind_time':'in 6 min'},
    {'name': '護国寺', 'position': (35.7189741,139.7253705), 'remind_time':'in 8 min'},
    {'name': '江戸川橋', 'position': (35.7097251,139.7307885), 'remind_time':'in 11 min'},
    {'name': '飯田橋', 'position': (35.7020837,139.7428291), 'remind_time':'in 14 min'},
    {'name': '市ヶ谷', 'position': (35.6910121,139.7333733), 'remind_time':'in 16 min'},
    {'name': '麹町', 'position': (35.6910121,139.7333733), 'remind_time':'in 18 min'},
    {'name': '永田町', 'position': (35.6786011,139.738092), 'remind_time':'in 20 min'},
    {'name': '桜田門', 'position': (35.6772949,139.7491844), 'remind_time':'in 22 min'},
    {'name': '有楽町', 'position': (35.6749187,139.7606258), 'remind_time':'in 24 min'},
    {'name': '銀座一丁目', 'position': (35.6743941,139.7647847), 'remind_time':'in 26 min'},
    {'name': '新富町', 'position': (35.6705851,139.7713176), 'remind_time':'in 28 min'},
    {'name': '月島', 'position': (35.663861,139.7818904), 'remind_time':'in 30 min'},
    {'name': '豊洲', 'position': (35.654825, 139.796324), 'remind_time':'in 32 min'},
    {'name': '辰巳', 'position': (35.6457723,139.8081529), 'remind_time':'in 34 min'},
    {'name': '新木場', 'position': (35.6459065,139.8245976), 'remind_time':'in 37 min'},
]

def exec_button(request):
    if 'x-soracom-token' not in request.headers:
        return 'Not from soracom button.'
    
    # Decode jwt token. (algorithm : RS256)
    request_data = jwt.decode(request.headers.get("x-soracom-token"), verify=False)
    position = (request_data['ctx']['location']['lat'], request_data['ctx']['location']['lon'])

    message = 'そろそろ着くぞ！起きろ！'

    # Add reminer to Slack.
    url = 'https://slack.com/api/reminders.add'
    method = 'POST'
    data = {
        'text': message,
        'time': get_remind_time(position)
    }
    headers = {
        'Content-Type': 'application/json;charset=utf-8',
        'Authorization': 'Bearer ' + os.environ.get('slack_oauth_access_token', ''),
    }
    print(data['time'])

    req = urllib.request.Request(url, data=json.dumps(data).encode(), method=method, headers=headers)
    with urllib.request.urlopen(req) as res:
        body = res.read()
        print(body)
    
    return 'Success.'

def get_distance(target_position, station_position):
    # 三平方の定理から距離を計測.緯度と経度の数値から出した座標上の距離なので単位はmではない
    return math.sqrt((1000 * target_position[0] - 1000 * station_position[0]) ** 2 + (1000 * target_position[1] - 1000 * station_position[1]) ** 2)

def get_remind_time(latest_position):
    distances = [get_distance(latest_position,x['position']) for x in stations]
    nearest_station = stations[distances.index(min(distances))]
    return nearest_station['remind_time']
```

requirements.txtも編集が必要です。
pyjwtを用いるため、`requirements.txt`に以下記載をしましょう。(ここに記載した内容が pip installされるイメージです)

```python:CloudFunctions(requirements.txt)
pyjwt
```

これを保存しただけだと、環境変数slack_oauth_access_tokenが設定されていないので、実行してもエラーになります。後ほど作成します。

発行されたCloudFunctionsへのURLを取得しておきます。

## 簡易位置情報をGoogle Cloud Functions宛に送るよう設定する
SORACOMコンソールからButton設定とFunk設定を行います。
Button設定とはSORACOMから Cloud Functionsへの送信データに位置情報を含める設定です。
Funk設定とはCloud Functionsへの宛先URLの指定です。
[こちらの設定](https://qiita.com/sousoumt/items/8c7d28c42f8a47c41831#soracom-enterprise-button%E8%A8%AD%E5%AE%9A)を行いましょう。
CloudFunctionsのURLを誤らないように注意です。

## 所要時間後にリマインドするようSlackのキー取得する
SlackにリマインドをAPIから追加できるのですが、それにはキー情報が必要です。
Slackアプリ作成,権限付与,キー情報取得を行います。

[Slack APIのページ](https://api.slack.com/) に接続します。Start Buildingします。
<img width="1120" alt="Slack_API01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/0e2928bf-d486-65b8-52f2-6c29c0cc95dd.png">

SORACOM Enterprise Button Serviceとでもしましょう。通知したいワークスペースを選択します。
<img width="562" alt="Slack_API02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/a912c824-9c52-3d79-404c-76182d788257.png">

Features -> OAuth & Permissionsに進みます。
<img width="746" alt="Slack_API03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/4c4f7808-d9cd-de8f-1ec3-14bd72ca4005.png">

Scopeの欄で`reminder:write`を選択します。
<img width="753" alt="Slack_API04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/84d830de-b61b-c44e-bd5b-3af86132ce87.png">

画面上部、`Install App to Workspace`を選択します。
<img width="704" alt="Slack_API05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/25ddf401-2ef9-9cf7-ee9b-e651a773b12a.png">

権限の確認画面です。許可するを選択します。
<img width="639" alt="Slack_API06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/b53bd302-6047-9e7c-c2fa-aa40c5fb0679.png">

`OAuth Access Token`が発行されるので、コピーしておきます。(★)
<img width="988" alt="Slack_API07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/367046be-03a1-dd59-f785-7bee525d8b8b.png">


## SlackのキーをCloudFunctionsに登録する
取得したSlackのキーを用いるよう、CloudFunctionsの設定を編集します。
Google Cloud Functionsの編集画面に進み、画面下部`環境変数、ネットワーキング・・・`を選択します。
<img width="486" alt="Cloud_Functions_01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/6b14738d-359d-c0f0-9fcf-224f6f01d17c.png">

環境変数を追加して、`slack_oauth_access_token`として(★)でコピーした`OAuth Access Token`を貼り付けます。
<img width="486" alt="Cloud_Functions_02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15264/d6bfd3dd-6a0b-23d2-5ed0-7938b495a244.png">

デプロイしましょう。
これで構築は終了です。正常にいけばButtonを押すと場所に応じてリマインドが作成されます。

# ソースコード補足

* 自分で構築する場合、当たり前ですが位置と所要時間の対応表は各自で埋めてください。
* 簡易位置情報から近隣駅の距離測定については、緯度と経度から単純に三平方の定理から算出しています。雑のように思えるでしょうが、そもそも自分の位置を基地局の位置に近似しているようなものなので、これで十分かなと思っています。２点間距離を測るために別APIを呼び出したり、地球の半径を気にするのはやりすぎでしょう。
* 駅間が遠くてリマインダーの時間ズレが大きくなることを懸念する場合は、途中の地点に架空の駅名を入れてしまいましょう。
（市ヶ谷駅ー飯田橋駅　間に「外堀駅」とか、上呂駅ー下呂駅　間に「中呂駅」とか。唐突の岐阜。）


# オチ

テストも兼ねて、試しに同じ方面に帰る後輩に渡し、使ってもらいました。
　私「どう？通知来た？」
後輩「通知は来ましたが、**通知に気が付かないほど泥酔していた**ので結局寝過ごしました！」

えー読まれている方の中で、どなたか
「**泥酔している人を起こす程度の電流値**」をご存じの方いらっしゃいますでしょうか。


---


ここからはアーキテクチャについての考察です。長いしほとんどSORACOM関係ない笑

# 考察
アーキテクチャに関する自己ツッコミもとい考察です。

## IFTTT(IF Location THEN Slack または IF Location THEN Webhook)での現在地把握では駄目なのか？
ボタンを押さないようにするために代替案です。IF Locationとは、ある場所に入ったら〜　を指定できる方法です。

まず、地下鉄ではモバイル端末のGPSが捉えにくく、恐らく失敗するでしょう。割と致命的です。
また、仮に地上の区間であっても、SlackのLocation設定を駅ごとに細かく設定するのはNGでしょう。
Slackが位置を確認するタイミングでピンポイントで領域に入っていないといけないため、あまり有効な手段ではありません。
必ず通る中間地点に場所を登録したら、復路でなく往路のときにも反応する点もNGポイントです。
あと、これはIFTTTのUIの都合ですが、Then Slackだとリマインドの設定まではできないです。

一方でThen Webhookにして別に受け取ってLambdaなりに渡せば自由度が増して改良はしますね。
午前中ならFunction終了とかにもできますし、リマインド設定対応はできまぁす。
が、朝まで飲んでいたときどうするんだ！気に入らない！没だ没！（強引

## SORACOM LTE-M Button powered by AWSは使えないのか？
通称あのボタンは使えないか？ですが、あのボタンでは位置情報がとれません。おわり。

## 交通系の経路検索APIを用いてはどうか？
これはちょっと採用するか悩みました。例えばGoogleのDirections APIでmodeパラメータをtransit(公共交通機関使用)にしてリクエストを投げるとトータルの所要時間や経路情報が帰ってきます。
これだと任意の地点からの検索になるから対応表不要になっていいじゃないかと思うかもしれません。
ただ、簡易位置情報で取得した位置情報のポイントが、駅のホームを指すわけではありません。だいたいの位置を指すわけです。これにより、仮に駅に居る時にボタンを押したとしても、駅まで歩く時間を経路トータル時間に含める形になります。その時間も含めると、トータルの時間に微妙なズレが含まれてきます。しかもリマインドが遅延する方向にズレます。
となると、一旦回避策として、位置情報から近隣駅の抽出を先に行い、その駅から目標駅への乗り換えをAPIで検索すると良さそうです。

・・・ですが、それでもまだ別の問題があります。

* 経路検索が想定のものと異なるとどうでしょうか（経由駅が違うなどのこと）
* 近隣駅が新宿だと思ったら実は新宿三丁目の駅のほうが近かったらどうでしょうか。

それだと近隣駅リストも結局入れなければならないことになります。
そこまで考えたら近隣駅リストとそこからのリマインド時間を指定したほうがシンプルじゃないか、ということでこうなりました。
ボタンを押したら経路検索してSlackに書き込んでくれるくらいは別途記事にするかもです。
ですが結局　経路検索結果の「思ってたんと違う」課題は残るままなので、ユーザ体験としては、目標駅までのデフォルト検索ルートがわかる、程度にしておけばいいのかもしれませんね。
もはや別アプリですねこれ笑

このあたり細かく考えられるのは、やはり東京近辺に住んでいるからなのかもしれません。
ええい細けぇことは(ry　と本来すぐ言ってしまう人間なのですが。

## 電車が走っている最中に押したらリマインド時間がズレないか？
駅間でボタンを押した場合、近隣の駅が目標駅から遠い側の駅とされてしまい、リマインド時間が遅れるのでは？という懸念。
これは、早くなる方向、遅くなる方向どちらにも転ぶ可能性のあるズレなので、先程のズレよりは問題として大きくはないと思いますが、確かにズレは発生しそうです。
一旦対策としては、リマインド時間を実質一つ前の駅にしたほうが実用的だと思います。

まぁ、あれですよ。

* ボタンを押してからFunctionを叩くまでの時間がかかること(俗に言うなんやかんやタイム。これがあるからボタンがコールドスタンバイとなり待機電力を使わず省電力で済むのですが）
* 駅間の長い箇所は結局複数点取って計算などするよりも中間点に擬似的な駅を用意したほうが実用的なこと

これらを考慮すると、ええい細けぇことは(ry　

## リマインド時間にCron起動したほうが正確ではないか？
と書きつつまた時間の正確性について。Cronサービスを使って通知をしてあげないの？という点。
AWSならCloudWatch Events, GCPならCloud Schedulerを用いれば時間起動は確実にできるでしょう。
しかしやらないといけない内容に対して採用技術が多くなりすぎるのも考えものだなということで却下しました。
Lambda/Cloud Functionsの設定も複数必要になるわけですし。（リマインダーをセットする関数とリマインダーが起動したら動作する関数。）

## Slack以外の通知方法は？
携帯への通知についてですが、Slack以外の方法もいいですね。

* Email送信 AWS Simple Email Serviceなど
* 電話をかける Amazon Connect
* その他モバイル端末への通知 Firebase Cloud Messaging, LINE ...

メール通知はシンプルでいいかもしれません。
電話も受電するまでの時間しばらく通知されるので、人を起こす意味では使えるかもです。
LINEへの通知は[SORACOM UG Explorer 2019](https://explorer2019.soracom-ug.jp/)でハンズオンとしてありましたね。

今回はSlackのreminder.addエンドポイントがあるのだから使ったらええやん、ということです。
高度な技術よりも使い勝手の良い技術が採択される例ですね。若干Slackアプリの登録が面倒だったというのはありますが。

## SORACOM Advent CalendarなのにSORACOMに関する内容薄くない？
アーキテクチャとは関係なくメタな意味ですが。
実はその通りです。しかし、それでもいいと私は思います。
それだけ**SORACOMが通信機能を簡単に使えるよう整備してくれているのだ**と考えていますから。
事実それ以外の開発に注力することができましたし。

はーすっきりした。

SORACOM及びUGの皆様、今年もお世話になりました。
来年もよろしくお願いします。