はじめに
この記事はフィヨルドブートキャンプ Advent Calendar 2020の 21 日目の記事です。
@shimewtrと申します。
普段は Ruby on Rails を用いてスマートフォンアプリのバックエンド開発をしています。
世界中のエンジニアは外出するとき、極力持ち物を減らしたいと思っていますよね?
もちろん私も思っています。
家の鍵にスマートロックの SESAME をつけてスマートフォンから鍵の解錠・施錠をできるようにしています。
鍵やかばんといった不要なものを持たず、スマートフォンだけを持って外出することを心がけています。
SESAME には大変お世話になっているのですが公式アプリの挙動に不満があります…。
公式アプリは鍵の状態確認が長く、接続するまでに時間がかかるため、家の扉の前で接続を待って立ちすくむことが何度もありました。
(家に近づくと Wi-Fi を拾ってしまい、接続し直すということもあります…。)
SESAME の APIで鍵を操作するとステータス確認をせず直接鍵の操作を行えるため、API から操作して不満を解消しようと考えました。
つくったもの
構成の概要は以下です。
- LINE 上のボタンをタップ
- Webhook で Lambda を発火
- SESAME API を叩く
- 結果を Lambda に返す
- 結果に応じて LINE で返信するテキストを返す
- LINE でメッセージを返す
我が家は私と妻の二人暮らしで、妻は IT に明るくないです。
アプリの追加や複雑な操作を必要とせず、日頃から使っている LINEから鍵の操作を行えるようにしました。
↓の画像のように LINE Messaging API を用いるとボタン上の UI を簡単に作れます。
(下部の鍵のアイコンがそれぞれボタンになっています。)
また、LINE からのメッセージを受け取り、SESAME の API を叩く処理は Lambda で実装しています。API Gateway で LINE からの Webhook の受け口を簡単に用意でき、十分な無料枠があり、費用をかけずに実装できるためです。
LINEの設定
LINE Developersのサイトから登録しましょう。
LINE のアカウントを持っていればすぐに登録できます。
登録が済んだら LINE の API を使えるように設定をしていきます。
Create a new provider を選択し、必要事項を入力後、
Create a Messaging API channel を選択して再度必要事項を入力すると自分の channel を開設できます。
Messaging API タブに表示される ID で検索するか、QR コードから友達追加できます。
同じく、Messaging API タブにある Webhook settings から Webhook の送り先の URL を設定できます。
メッセージの送信に対するイベントはもちろん、友達追加、友達解除など多数のイベントをトリガーに Webhook が送信されます。
詳しくはこちらをご確認ください。
今回は Lambda で LINE からの Webhook を受けて処理を行うため、Lambda に追加した API ゲートウェイのエンドポイントを設定しています。
受け取った Webhook イベントに対して返信をする際などに、 Messaging API タブの最下部にある Channel access token が必要です。
トークンを発行してメモしておきましょう。
LINE のトーク画面下部のメニューは LINE Official Account Manager から設定できます。
ボタンとして表示するテキストや画像、ボタンタップ時のアクションなどを設定できます。
今回は鍵が開いた画像をタップすると「鍵あけて」、鍵が閉まった画像をタップすると「鍵しめて」というテキストを送信するようにしています。
ここで設定したテキストに応じて Lambda で処理を行います。
コード
Lambda で実行しているスクリプトは以下です。
以下の環境変数を設定しています。
AUTH_TOKEN
- SESAME API のアクセストークン
KEY_TOKENS
- SESAME の ID
- 我が家では 2 つの SESAME を使用しているため 2 つ設定
LINE_CHANNEL_ACCESS_TOKEN
- LINE の Channel access token
AUTHORIZED_USERS
- LINE のユーザー ID
- 私と妻の 2 人分設定
import json
import os
import random
import requests
import sys
import time
from linebot import LineBotApi
from linebot.models import TextSendMessage
AUTH_TOKEN = os.environ['AUTH_TOKEN']
KEY_TOKENS = [os.environ['KEY_TOKEN_1'], os.environ['KEY_TOKEN_2']]
LINE_CHANNEL_ACCESS_TOKEN = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
AUTHORIZED_USERS = [os.environ['USER_ID_1'], os.environ['USER_ID_2']]
class LinkingLineSesame():
HEADERS = {
'Authorization': AUTH_TOKEN,
'Content-Type': 'application/json'
}
SESAME_API_URI = 'https://api.candyhouse.co/public/'
SESAME_COMMAND = {
"鍵あけて": {
"send_message": "鍵をあけました",
"command": '{"command":"unlock"}',
},
"鍵しめて": {
"send_message": "鍵をしめました",
"command": '{"command":"lock"}',
},
}
UNAUTHORIZED_MESSAGES = [
'ユーザー認証に失敗しました',
]
def __init__(self, event):
body = json.loads(event['body'])
self.text = body['events'][0]['message']['text']
self.user_id = body['events'][0]['source']['userId']
self.reply_token = body['events'][0]['replyToken']
self.sesame_command = self.SESAME_COMMAND.get(self.text)
if not self.user_id in AUTHORIZED_USERS:
self.send_message = random.choice(self.UNAUTHORIZED_MESSAGES)
elif bool(self.sesame_command):
self.send_message = self.sesame_command.get("send_message")
self.command = self.sesame_command.get("command")
self.control_sesame()
else:
self.send_message = "鍵を操作するにはメニューのボタンをタップしてください。"
self.reply_message()
def control_sesame(self):
task_ids = self.post_sesame_control()
self.check_sesame_status(task_ids)
def post_sesame_control(self):
task_ids = []
try:
for key_token in KEY_TOKENS:
uri = self.SESAME_API_URI + 'sesame/{}'.format(key_token)
res = requests.post(uri, headers=self.HEADERS, data=self.command)
task_ids.append(json.loads(res.text)['task_id'])
except requests.RequestException as e:
print(e)
self.send_message = '鍵の動作中にエラーが発生しました。'
return task_ids
def check_sesame_status(self, task_ids):
if len(task_ids) < len(KEY_TOKENS):
self.send_message = '鍵が正しく作動していません'
return None
try:
attempts_num = 3
for i in range(attempts_num):
time.sleep(5)
for task_id in task_ids:
api_uri = 'https://api.candyhouse.co/public/action-result?task_id={}'.format(task_id)
res = requests.get(api_uri, headers=self.HEADERS)
res = json.loads(res.text)
if res['status'] == "terminated" and res["successful"] == True:
task_ids = [i for i in task_ids if i == task_id]
if len(task_ids) == 0:
break
elif i == attempts_num - 1:
self.send_message = '鍵のステータスが正しく取得できません。'
except requests.RequestException as e:
print(e)
self.send_message = '鍵のステータス取得中にエラーが発生しました。'
def reply_message(self):
line_bot_api = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)
line_bot_api.reply_message(
self.reply_token, TextSendMessage(text=self.send_message))
def lambda_handler(event, context):
print("Received event: " + json.dumps(event, indent=2))
LinkingLineSesame(event)
return 'finished'
ユーザーの判定
if not self.user_id in AUTHORIZED_USERS:
self.send_message = random.choice(self.UNAUTHORIZED_MESSAGES)
ユーザーの認証を上記で行っています。
環境変数に設定したユーザー ID 以外のユーザーからのメッセージの場合、UNAUTHORIZED_MESSAGES
で設定したメッセージを送信して処理を終えます。
LINE Channel はプライベートにできないため(やり方を知っている方がいれば教えて下さい)、 ID がわかれば誰でも友達登録できます。
他の人が我が家の鍵を解錠・施錠できないようにユーザー ID で判断しています。
また、API Gateway の設定で LINE からのリクエスト以外は許可しない設定をし、Lambda を無駄に実行しない設定をすると安心です。
SESAMEの操作
def control_sesame(self):
task_ids = self.post_sesame_control()
self.check_sesame_status(task_ids)
def post_sesame_control(self):
task_ids = []
try:
for key_token in KEY_TOKENS:
uri = self.SESAME_API_URI + 'sesame/{}'.format(key_token)
res = requests.post(uri, headers=self.HEADERS, data=self.command)
task_ids.append(json.loads(res.text)['task_id'])
except requests.RequestException as e:
print(e)
self.send_message = '鍵の動作中にエラーが発生しました。'
return task_ids
SESAME の操作は上記で行っています。
2 つの鍵に対して、送信されたテキストに応じて鍵を操作しています。
鍵を操作する API のレスポンスは操作するコマンドを送信できたか否かしかわからないため、実際に鍵の操作を正しく行えたかは別の API を叩く必要があります。
鍵の状態確認
def check_sesame_status(self, task_ids):
if len(task_ids) < len(KEY_TOKENS):
self.send_message = '鍵が正しく作動していません'
return None
try:
attempts_num = 3
for i in range(attempts_num):
time.sleep(5)
for task_id in task_ids:
api_uri = 'https://api.candyhouse.co/public/action-result?task_id={}'.format(task_id)
res = requests.get(api_uri, headers=self.HEADERS)
res = json.loads(res.text)
if res['status'] == "terminated" and res["successful"] == True:
task_ids = [i for i in task_ids if i == task_id]
if len(task_ids) == 0:
break
elif i == attempts_num - 1:
self.send_message = '鍵のステータスが正しく取得できません。'
except requests.RequestException as e:
print(e)
self.send_message = '鍵のステータス取得中にエラーが発生しました。'
上記がステータスを確認している部分です。
鍵を操作する API を叩いてから実際に鍵が動作を終えるまではタイムラグがあるため、一定時間ごとにコマンドの実行状況を確認し、正常に動作が終了したかを判定しています。
まとめ
LINE から家の鍵の解錠・施錠ができるようになりました。
これまでは接続に時間がかかり家の前で待ちぼうけになっていましたが、LINE を起動してワンタップで鍵の解錠・施錠を行えるため非常に快適です。
ちょっとした不便をプログラミングの力で解決できるのでぜひ試してみてください!
ご意見やご感想は@shimewtrまでお願いいたします。