スマートロック sesame を line から開閉してみた

## この記事について
この記事は Akatsuki Games Advent Calendar 2023 の8日目の記事です。
Advent Calendar の7日目はなかひこくんの
[Terraform で CloudRun + Identity-Aware Proxy をやっていく 2023](https://hackerslab.aktsk.jp/2023/12/07/105429) でした。
記事内にある基盤は本当にお世話になっていて、自分もクライアントの立場から貢献をしていきたいところです…！


## 作るきっかけ

自分は 自宅のドア と アパートのエントランスドア（この規模では珍しい気がする）を開ける解錠ボタン にスマートロックのsesameを使用しています。
また、wifiモジュールを使用してbluetooth圏外からも操作できるようにしています。
ただ操作はsesame公式のアプリを使用していたのですが、wifi経由だと反応しない場合が何回かあり家に入りたいのに入れない！という状況があったためより使いやすい手段を探す事にしました。

## 使用するもの

- line bot
- aws lambda
- python3.10.11

line botを選んだ理由は
- lineの公式アカウントとかでメニューからメッセージを送る機能が個人的にはわかりやすかった
- line botにちょっと興味があった

aws lambdaを選んだ理由は
- こちらも単純に興味から

## フロー
このようなフローでとりあえず実装をしていきます 
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/96c65d18-6204-52c8-0ce8-c23eaa72a392.png">

## line botの構築
### 構築ページ
https://developers.line.biz/en/

上記リンクにアクセスして、ログインをします

#### 新規プロバイダーの作成

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/0c56335f-85f4-36d9-9198-374a8316381e.png">

Line Developerトップ から Providersの赤枠Createを選択します

#### Messaging API channelの作成 
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/b28d43de-eaa8-e74c-66e1-6e6929bb5301.png">

今回はメッセージのやり取りを行うので `Messaging API channel` を選択します 

### アクセストークンの作成

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/d14be1a4-f27e-9968-3554-e31d086c9675.png">

作成したchannelを開いて、Messaging APIを選択します

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/99218497-9088-06af-0868-2d6d27dffdd4.png">

一番下にアクセストークンを発行する項目があるので、`Issue` を押して発行します
なお、出てきた文字列はメモをしてください

### account managerへ移動する
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/9c753985-1c71-59e1-579a-151a0e8fb3ce.png">

作成したchannelのページから、 赤枠の `Line Official Account Manager` へアクセスします

## line account managerでメニューを構築する

### リッチメニューの作成

#### 作成
<img width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/2131491f-a817-5d2d-fbba-b89a81873db9.png">

account manager の タブから `ホーム` を選択して、左のメニューから `リッチメニュー` を選択します
すると、リッチメニューを作成 と出ているので選択します

#### タイトルと表示期間を設定
<img width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/f31449cc-109c-6591-84c3-8e20bafcda8b.png">

とりあえず 2037年まで有効にしておきます

#### テンプレートと画像を変更する
<img width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/0273ad6d-8891-28f0-be9a-b96f050b412b.png">

テンプレートは３つのボタンに

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/6439979c-0ef8-08d9-7340-e46d50a7c374.png">

`画像` を選択すると、全体かエリア毎が出てくるのでエリア毎にします 


<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/cf0b60d3-5892-c177-6e67-a98e0d4a2aff.png">

とりあえず単色塗りつぶしをしておきます。 

#### 押した時のアクション

<img width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/bf208203-b0e0-e3e9-73b5-4922368e2610.png">

タイプを `テキスト` にして、開ける、閉める、エントランス用で設定します
ここのテキストは後ほど実行するコードで合わせる必要があります。

### webhookの有効化

<img width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/0777d082-70cc-8f37-4fa2-d73c2947f53b.png">

上のタブの右側にある`設定`を選択して 左のメニューから `応答設定` を選択して `応答機能` にある `Webhook` のみ有効にしておきます

## sesame apiにアクセスするための設定

### api利用のために登録
https://partners.candyhouse.co/login/
上記リンクにアクセスしてログインをします

### Keyを取得する
<img width="700" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/6f583a34-67cf-5a25-e398-461a90fa9c8b.png">

- `ご利用中のapi key` をメモしておく
- 既にデバイスを登録している場合は、赤枠の `user devices` を選択してデバイスを取得します
- 右側の赤枠のデバイスを選択すると、UUIDとSecret Keyが表示されるのでメモをしておきます


 
## AWS Lambdaで実装する


### 使用するpythonのライブラリをまとめる
#### 仮想環境にインストール
必要なライブラリのみzip化していくので仮想環境内にインストールしていきます
``` bat
mkdir aws-line-bot
cd aws-line-bot
python -m venv venv
./venv/scripts/activate 
python -m pip install line-bot-sdk
```

#### zip化
仮想環境にline-bot-sdkをインストールしたら、これをzip化していきます
1. pythonフォルダを作成し、`./venv/Lib/site-packages`下のフォルダをpythonフォルダへコピーします
2. pythonフォルダをzip化

#### レイヤーにライブラリを登録する
![スクリーンショット 2023-12-08 060038.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/87d19b89-3db7-bd44-4a35-5cff6c508dae.png)
aws Lambdaにアクセスしたら、左のメニューから `その他のリソース` -> `レイヤー` -> `レイヤーの作成` を選択します


<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/fae933dd-c1b9-74de-577d-da5ed2924fa0.png"> 

先程zip化したpythonライブラリを登録します。
また、今回はpython3.10 + windows11を用いて作成したのでpython 3.10とx86_64を指定します

#### 関数を作成する

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/5e1b3216-18c5-97b8-48ea-9042224ba4a9.png"> 

では、本題の関数を作成します
AWS Lambda から 左のメニューより 関数 -> 関数の作成 を選択します

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/4876fd03-e82c-d0f3-eb02-e391d8162b4d.png">

一から作成を選択した後は、ライブラリの時と同じバージョンをとアーキテクチャを指定しておきます

##### 環境変数に必要な情報を登録しておく

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/a2877b9e-765c-00ba-fef5-e30d4b15d5a9.png">

関数を作成したら、まずは必要になる環境変数を登録していきます
- sesameのAPI Key
- sesameのデバイス毎のUUIDとSecret Key
- Line botのAccess Token
以上を登録します

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/c04d3cfd-fb3b-9d74-96a9-dfcc79574737.png">

自分はこんな感じになりました
sesame botはエントランスを開ける用のsesame botで
upperとlowerに分かれているのは玄関ドアには２つ鍵があるためです


#### 使用するレイヤーを登録する
ページ一番下に `レイヤー` があるので `レイヤーの追加` を選択します
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/19f1ac7b-c97b-d9ed-1a37-7f06b1e8c905.png">
先程zip化してアップロードしたレイヤーを追加

そして、もう一つ `pycryptodome` を使用するために下記ページからARNを持ってきます
https://github.com/keithrozario/Klayers/tree/master/deployments/python3.10

#### サンプルの実行コード

https://doc.candyhouse.co/ja/SesameAPI#web-api-%E3%81%AB%E3%82%88%E3%82%8B%E6%96%BD%E8%A7%A3%E9%8C%A0
sesame apiのドキュメントはこちら

ここで注意してほしいのが、sesame botを操作する場合のコマンドコードは `89` となります
ドキュメントには以下のコマンドがありますが、このコマンドでsesame botを動かすと回転量がおかしくなってしまいます。

```
コマンドコードリスト
toggle:88
lock:82
unlock:83
```
※ 1つ謝罪として89で動く事を発見したブログ記事があったのですが、今探しても全然見つからず参考に載せられませんでした。もしありましたらコメントに書き込んでいただけると助かります。

以下がサンプルの実行コードとなります
``` python
import os
import json
from linebot import LineBotApi
from linebot.models import TextSendMessage

# sesame apiと通信する時に使用する
import Crypto.Cipher.AES as AES
from Crypto.Hash import CMAC
import json
import base64
import datetime
import requests

LINE_CHANNEL_ACCESS_TOKEN = os.getenv('LINE_CHANNEL_ACCESS_TOKEN')
SESAME_KEY = os.getenv('SESAME_KEY')
# エントランスドアを開けるsesame bot
SESAME_BOT_ID = os.getenv('SESAME_BOT_ID')
SESAME_BOT_SECRET_KEY = os.getenv('SESAME_BOT_SECRET_KEY')
# 玄関のドアにあるsesame
SESAME_UPPER_ID = os.getenv('SESAME_UPPER_ID')
SESAME_UPPER_SECRET_KEY = os.getenv('SESAME_UPPER_SECRET_KEY')

bot_api = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)

def sesame_post(id, key, command):
    url = f'https://app.candyhouse.co/api/sesame2/{id}/cmd'
    headers = {"x-api-key" : SESAME_KEY} 
    
    message = int(datetime.datetime.now().timestamp()).to_bytes(4, byteorder='little').hex()[2:8]
    cmac = CMAC.new(bytes.fromhex(key), bytes.fromhex(message), ciphermod=AES)
    sign = cmac.hexdigest()
    body = {"cmd":command, "sign":sign,"history":base64.b64encode("Linebotから".encode()).decode()}
    # sesame web apiへリクエスト
    requests.post(url, headers=headers, data=json.dumps(body))
    
def lambda_handler(event, context):
    try:
        events = json.loads(event['body'])['events']
        # メッセージではない場合は終了する
        if events[0]['type'] != 'message':
            return {'statusCode': 200, 'body': json.dumps('成功')}
        # メッセージの種類がテキスト以外だった場合は終了する
        if events[0]['message']['type'] != 'text':
            return {'statusCode': 200, 'body': json.dumps('成功')}
            
        # line botから送られてきたメッセージを判定する
        messageText = events[0]['message']['text']
        if messageText == "鍵を開ける":
            sesame_post(SESAME_UPPER_ID, SESAME_UPPER_SECRET_KEY, 83)
        elif messageText == "鍵を閉める":
            sesame_post(SESAME_UPPER_ID, SESAME_UPPER_SECRET_KEY, 82)
        elif messageText == "エントランス開ける":
            sesame_post(SESAME_BOT_ID, SESAME_BOT_SECRET_KEY, 89)
        replyToken = events[0]['replyToken']
        bot_api.reply_message(replyToken, TextSendMessage(text="完了"))
    except Exception as e:
        print(e)
        return {'statusCode': 500, 'body': json.dumps('失敗')}
    return {'statusCode': 200, 'body': json.dumps('成功')}
```

#### デプロイ
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/57698906-6f0d-8124-8043-1a3e92e7a72b.png">

コードソースより `Deploy` を選択してデプロイを行い実装は完了となります

## AWS API Gatewayに登録

### 作成
関数が出来たらAPIのトリガーに登録をします。

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/5ac8a714-d1ca-ed4f-f1d2-e94329683d7a.png">

関数の概要より、 トリガーを追加を選択します

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/1639cb23-53f5-71ef-ee67-f599dd0fe313.png">

ひとまず動作を行いたいので、`Create a new API` と `REST API` を選び追加をします

### URLのコピー
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/6683efe2-7507-0f25-9d13-a9f4f95d81eb.png">

白塗りつぶしの部分にURLがありますので、コピーをします

## LinebotのWebhookにAPI URLを登録する
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/06e53ecb-6ba3-f82f-a607-025f5bbafea4.png">

Line Developersに戻り、`Webhook settings` より先程コピーしたURLを登録します

## 実行する端末からLinebotを友だち登録する
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/17d805c9-c462-8373-84d1-415cd06f69f5.png">

Line Developers のチャンネルから Messaging APIを開いて、QRコードを読み取ります
これで全ての作業が完了しました。

## 実際の動作の様子
色々と変えていますが、以下のように終わったら返答が返ってきます。

<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126834/233a6a6a-597d-187d-88fd-14fafdc6134c.png">

## 結果
目的にあった反応しないを解消するについては今のところ達成できています。
ただし、サンプルのままでは複数のデバイスを動かすと都度待ってしまいますので、async等を使用して非同期でそれぞれを実行するのが良いかと思います。

## 参考

- https://www.ryucom.co.jp/blog/aws/4713
- https://www.actie.smagai.com/%E3%82%BB%E3%82%B5%E3%83%9F%E3%81%AEapi%E3%82%AD%E3%83%BC%E5%8F%96%E5%BE%97%E6%96%B9%E6%B3%95/

明日の記事は Yusuke Nakajimaさんより UnityのProjectWindowに表示履歴機能を付けるエディタ拡張 とのことです。
エディタ拡張いいですよね！既存のProjectWindowにつけるということでどう付けるか気になるところです。