SESAME3をWEB API経由で操作する

いろいろあって会社オフィスの1FにSESAME3を導入しました。

SESAME3＋スマホの組み合わせでも十分便利だけど、やっぱりWEB API使いたい！
というわけで始めました。
（1Fだけはスマホで開けてね、って言っても社内から反発ありそうだし…）

さほど難解なところもないので、[公式ページ](https://doc.candyhouse.co/ja/SesameAPI)を見ながら進めれば大丈夫です。
（載せたコードもほぼ公式そのままです）

## 導入環境
 - Raspberry Pi 4
    - Ubuntu Server 20.04 LTS
    - Python 3.9.6
 - [SESAME3](https://jp.candyhouse.co/collections/frontpage/products/sesame3)
 - [SESAME3用 Wifiモジュール](https://jp.candyhouse.co/collections/frontpage/products/new-wifi?variant=33274961363007)

## WEB APIに必要な情報（SESAME3）
 - API Key（APIキー）
 - Secret Key（秘密鍵）
 - UUID

### API Key
まず[CandyHOUSEダッシュボード](https://dash.candyhouse.co/)にアクセスして、API Keyを入手しましょう。
以下画面にメールアドレスを入力すると、4桁の認証コードがメールで届きます。
![1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315032/de671411-7f13-3a06-a3d3-09f200618d32.jpeg)
届いた認証コードを入力してログインします。
![2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315032/901fc6ff-207d-1240-160b-48077a623ae6.jpeg)
ログイン後に表示される「API Key」の中の文字列をコピーしておきましょう。
（Client IDはいまのところ使い道がわからないです…）
![3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315032/0206954a-6c92-b524-73c4-a7eaf42cabf0.jpeg)

### Secret Key
秘密鍵の確認はちょっと面倒です。（もっと良い方法あれば教えてほしい）
SESAMEの専用アプリ「[セサミ、ひらけごま！](https://apps.apple.com/jp/app/%E3%82%BB%E3%82%B5%E3%83%9F-%E3%81%B2%E3%82%89%E3%81%91%E3%82%B4%E3%83%9E/id1532692301)」を利用します。
>「セサミ」などでストア検索するとアプリが2つ出てきますが、SESAME3では「[セサミ、ひらけごま！](https://apps.apple.com/jp/app/%E3%82%BB%E3%82%B5%E3%83%9F-%E3%81%B2%E3%82%89%E3%81%91%E3%82%B4%E3%83%9E/id1532692301)」を使います。
>もうひとつの「[SESAME セサミ](https://apps.apple.com/jp/app/sesame-%E3%82%BB%E3%82%B5%E3%83%9F/id1038881712)」は旧バージョンのSESAMEで利用するようです。
>※リンクはiPhoneアプリです。Android版は[こちら](https://play.google.com/store/apps/details?id=co.candyhouse.sesame2)。

アプリを起動して、SESAME3を登録したあとにメニュー画面から鍵のシェアをおこなうとQRコードが表示されます。
![5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315032/fa1eab7f-b56a-62ac-ee48-335ee7ecacb7.jpeg)
スマホ間で鍵を共有するときに利用するQRコードですが、これをスマホのQRコードリーダなどで読み取ると
`ssm://UI?t=sk&sk=xxxxxxxxxxxxxxxxxxxxx&l=0&n=xxxxxxxxxxx`
のような文字列が取得できるので、ここから`sk`の部分を抜き取ります。（秘密鍵）
パースをかけたりして抜き出すかたもいるようですが、自分は目視で抜き取りました。

>分解するとこんな感じ
>
>```bash
>t=sk
>sk=xxxxxxxxxxxxxxxxxxxxx
>l=0
>n=xxxxxxxxxxx
>```

のちほどこの`sk`をCMACで暗号化して利用しますので、文字列をコピーしておきましょう。
（これをそのままGithubに乗せてるっぽいの見かけるけど大丈夫なんかな…？）

### UUID
UUIDもSecret Keyと同じく専用アプリのメニュー内から確認できます。
メニューの一番下に表示されます。
![4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315032/f5f43622-8a19-ffba-5d03-68a94efcd602.jpeg)
こちらもUUID部分の文字列をコピーしておきましょう。


## WEB API動作
URL : `https://app.candyhouse.co/api/sesame2/`
### SESAME3のステータスを取得する
先程ダッシュボードで取得したAPI Keyをヘッダー（x-api-key）に入れて、GETすると現在のステータスが確認できます。
URL末尾の`UUID`には、アプリで取得した自分のSESAME3のUUIDを入れます。

```bash
% curl -H "x-api-key":"[myapikey]" https://app.candyhouse.co/api/sesame2/[UUID]
{"batteryPercentage":100,"batteryVoltage":6.0316715542522,"position":-121,"CHSesame2Status":"locked","timestamp":1628937684,"wm2State":true}%
```
バッテリー情報、サムターンのポジション、施錠状態などが確認できます。
（施錠・解錠ログを取得するAPIもありますが、今回は割愛）

### 解錠・施錠
公式ページを参照すると`BODY`に`cmd`、`history`、`sign`をつけてPOSTすれば良いとのこと。

```title:公式ページから抜粋
POST: "https://app.candyhouse.co/api/sesame2/${sesame_id}/cmd"
BODY: {
        cmd: cmd,
        history: base64_history,
        sign: sign
}
```
>※上の「${sesame_id}」はUUIDのこと

|BODY   |公式説明|備考|
|:------|:-----------|:-----|
|cmd    |開閉コマンドコード|toggle:88、lock:82、unlock:83|
|history|履歴|操作ログを見たときに表示される名前|
|sign   |署名         |秘密鍵をAES-CMACで暗号化した署名|
※`toggle`って何に使うんですかね…

## Pythonからアクセス
以下、Python3での動作です。
[pysesame3](https://pypi.org/project/pysesame3/)というSESAME3専用ライブラリもありますが、今回は利用していません。

### ライブラリ
今回使用するライブラリをインポートします。
事前に`pip`でインストールしておきましょう。

```bash
pip install pycryptodome requests
```

```python
import datetime, base64, requests, json
from Crypto.Hash import CMAC
from Crypto.Cipher import AES
```

### パラメータ（APIキー、秘密鍵、UUID）
先程取得した各種パラメータを使います。

```python
# 各種パラメータ
uuid = 'my-uuid'
secret_key = 'my-secret_key'
api_key = 'my-api_key'
```

### BODY
まずは開閉のコマンド`cmd`に値を入れます。
今回は`lock:82`で施錠します。

```python
# cmd
cmd = 82
```

続いて`history`は自分でわかりやすい名前をつければ良いだけなので、「WEB API」としました。
複数ノードからAPIを叩く場合には、判別できるように名前を区別したほうがよいかもしれません。
base64でエンコードしてから`body`に入れます

```python
# history
history = 'WEB API'
base64_history = base64.b64encode(bytes(history, 'utf-8')).decode()
```

署名部分は秘密鍵を`CMAC`を利用して暗号化します。
タイムスタンプを追加してから、HEXのダイジェスト値を作ります。

```python
# sign
cmac = CMAC.new(bytes.fromhex(secret_key), ciphermod=AES)
ts = int(datetime.datetime.now().timestamp())
message = ts.to_bytes(4, byteorder='little')
message = message.hex()[2:8]
cmac = CMAC.new(bytes.fromhex(secret_key), ciphermod=AES)
cmac.update(bytes.fromhex(message))
sign = cmac.hexdigest()
```

あとはAPIリクエストをセットして完了です。

```python
# API
url = f'https://app.candyhouse.co/api/sesame2/{uuid}/cmd'
body = {
    'cmd': cmd,
    'history': base64_history,
    'sign': sign
}
res = requests.post(url, json.dumps(body), headers=headers)
```

---
## コード全体
実際の運用では、クラスオブジェクトにしてレスポンスの確認やエラー処理などを追加していますが、単体の動作確認だけであればこのコードだけでも十分です。

```python
import datetime, base64, requests, json
from Crypto.Hash import CMAC
from Crypto.Cipher import AES

# 各種パラメータ
uuid = 'my-uuid'
secret_key = 'my-secret_key'
api_key = 'my-api_key'

# ヘッダーの設定
headers = {'x-api-key': api_key}

# cmd
cmd = 82 # 施錠する場合は「82」、解錠する場合は「83」

# history
history = 'WEB API' # とりあえず「WEB API」と名付ける
base64_history = base64.b64encode(bytes(history, 'utf-8')).decode()

# sign
cmac = CMAC.new(bytes.fromhex(secret_key), ciphermod=AES)
ts = int(datetime.datetime.now().timestamp())
message = ts.to_bytes(4, byteorder='little')
message = message.hex()[2:8]
cmac = CMAC.new(bytes.fromhex(secret_key), ciphermod=AES)
cmac.update(bytes.fromhex(message))
sign = cmac.hexdigest()

# API
url = f'https://app.candyhouse.co/api/sesame2/{uuid}/cmd'
body = {
    'cmd': cmd,
    'history': base64_history,
    'sign': sign
}
res = requests.post(url, json.dumps(body), headers=headers)
```



## 注意すること
詳細は公開されていないようですが、WEB API利用数に制限があるようです。
正確な数字はわかりませんが、1アカウントにつき900-1000回くらいのようです。1日放置したら制限は解除されていました。

CANDYHOUSE様に問い合わせたところ、WEB APIの有料化が予定されていて、その後有料ユーザは利用制限が解除されるそうです。
現時点でWEB API利用は無料です。（2021年8月時点）

利用頻度が高い場合は注意です。締め出されたり閉じ込められたりします…。