スマートロックのセサミをさらに便利にする(アレクサからパスコードレスで解錠&オートロック化)

# 身の回りの困りごと
我が家のスマートロックがちょっとスマートじゃないこと

# 我が家のスマートロック事情
- [Echo Dot 第三世代](https://www.amazon.co.jp/dp/B07PFFMQ64?ref_=cm_sw_r_cp_ud_dp_A978H58EDSJ4ZH048NX3)
- [SESAME3](https://jp.candyhouse.co/products/sesame3) * 2 (ドアの鍵がダブルロックなので2つ必要)
- [SESAME bot](https://jp.candyhouse.co/products/sesame3-bot) * 2 (マンションの入り口のオートロック解除のために)
- [SESAME WiFiモジュール](https://jp.candyhouse.co/products/new-wifi)

### なぜSESAMEを使うのか
- 製品が全体的に安い 圧倒的に安い
    - ダブルロックでSESAMEも2つ必要だしマンションの入り口のオートロック解除用にbotも2つ必要なので値段は大事
- APIが公開されている

### しかし。。。
SESAMEはまだ**完璧ではない**

ギリギリ族の私にとって鍵を開ける時間も閉める時間も惜しいのです
早起きは無理なのです

# 具体的に何に困っているのかというと

### 1. Alexa経由でのSESAMEの解錠がスマートじゃない
公式のAlexaスキルでSESAMEを解錠しようとするとパスコードを要求されるため鍵を開けるのに2回指示しないといけない
しかも、我が家はダブルロックでSESAMEが2つあるのでAlexaに4回指示する必要がある

```
私「アレクサ、『上の鍵』を開けて」
アレクサ「『上の鍵』の確認コードはなんですか？」
私「xxxx」
アレクサ「ロック解除しています。お待ちください」
鍵「ｳｨｰﾝｶﾞﾁｬﾝ」

私「アレクサ、『下の鍵』を開けて」
アレクサ「『下の鍵』の確認コードはなんですか？」
私「yyyy」
アレクサ「ロック解除しています。お待ちください」
鍵「ｳｨｰﾝｶﾞﾁｬﾝ」
```

なんてめんどくさいんだ！！！

### 2. SESAMEのオートロックは秒数設定しかできないのでスマートじゃない
設定秒数が短すぎると荷物を受け取ってる間に施錠されてしまったり
設定秒数が長すぎるとドアが閉まってから施錠されるまで待たされたり

かといって本当の意味でのオートロック機能がついてるスマートロックは高い。。。

# 理想を追い求めてみる
#### ゴール1. Alexaへ1回の指示だけで2つの鍵を解錠できること

#### ゴール2. ドアが閉まったら鍵が閉まること

# Alexaへ1回の指示だけで2つの鍵を解錠できるようにする
具体的にどうするかと言うと
`Alexa → IFTTT → GAS → SESAME API` という流れで解錠できるようにする

## 1. `GAS → SESAME API` の連携
### 1-1. SESAMEのAPI利用に必要なパラメータとGASのエンドポイント認証用のハッシュ値を用意する
- SESAMEの`uuid`と`secretKey`と`apiKey`を取得する([参考](https://zenn.dev/key3/articles/6c1c2841d7a8a2))
    - 今だと[ドキュメント](https://doc.candyhouse.co/ja/SesameAPI)に記載されている[APIKEYを取得できる管理画面](https://partners.candyhouse.co/)から`uuid`や`secretKey`も結構簡単に取得できるみたい
- 任意の文字列をsha512でハッシュ化し、GASのエンドポイントで認証するためのハッシュ値を作る
    - Macならターミナルから `shasum` で簡単に生成できます(サンプルなのでくれぐれもこのハッシュ値は使わないように。。。。)
    - ここでは行いませんが[ストレッチング](https://medium-company.com/%E3%82%B9%E3%83%88%E3%83%AC%E3%83%83%E3%83%81%E3%83%B3%E3%82%B0/)などもお好みで
        ```shell
        $ echo -n 'oreno_password_daze' | shasum -a 512
        40fc44138fa114b4c7a604052e8e0f7f105c1123ab7b8410870ade930b87857f0c339e52f24f8d76265cacfaddbdce7cd8dd2cbfce5633c742f5e1aa7d4bcb79  -
        ```

### 1-2. 秘匿情報をGASのスクリプトプロパティに設定する
先ほど用意したapiKeyなどは秘匿情報なのでソースに書かず以下のような形でスクリプトプロパティに設定する(プロジェクトの設定の中)

```
API_KEY: YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
DEVICES: [{ "uuid": "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA", "secretKey": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}, { "uuid": "BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB", "secretKey": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" }]
PASSWORD_DIGEST: 40fc44138fa114b4c7a604052e8e0f7f105c1123ab7b8410870ade930b87857f0c339e52f24f8d76265cacfaddbdce7cd8dd2cbfce5633c742f5e1aa7d4bcb79
```

![スクリーンショット 2022-12-08 23.46.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/3c355b62-91a4-6801-7a24-311b9a282c84.png)
![スクリーンショット 2022-12-08 23.49.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/9569b069-c121-fe72-ea32-6d3214ca7e72.png)


### 1-3. スクリプトプロパティから秘匿情報を取得する処理を書く

`DEVICES` はオブジェクトの配列で扱うためにJSONで保存しているのでパースします

```js:credentials.gs
const passwordDigest = PropertiesService.getScriptProperties().getProperty('PASSWORD_DIGEST');
const apiKey = PropertiesService.getScriptProperties().getProperty('API_KEY');
const devices = JSON.parse(PropertiesService.getScriptProperties().getProperty('DEVICES'));
```

### 1-4. GASで`AES-CMAC署名`できるようにする
- [SESAMEのAPIを叩くためには`AES-CMAC署名`が必要](https://doc.candyhouse.co/ja/SesameAPI)だがGASそのままではできないので[こちら](https://zmzlz.blogspot.com/2021/06/sesame3-sesame-os2-gas-web-api.html)を参考にして`cryptojs-extention`からコードを頂く

```js:cryptojs-aes.min.js.gs
/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
var CryptoJS=CryptoJS||function(u,p)....割愛
```

```js:cmac.min.js.gs
/*
 * The MIT License
 *
 * (MIT)Copyright (c) 2015 artjomb
 */
!function(t){var r;r=t.hasOwnProperty(....割愛
```

### 1-5. 認証とSESAMEのAPIを叩く処理を書く
- [GASでのハッシュ化の参考リンク](https://qiita.com/tatataichi/items/7e3a27e1b69200d3fd86)
- 署名処理は引き続き[こちら](https://zmzlz.blogspot.com/2021/06/sesame3-sesame-os2-gas-web-api.html)を参考にさせて頂きました

```js:main.gs
function doPost(e) {
  const params = JSON.parse(e.postData.getDataAsString());

  const encodedUserName = Utilities.base64Encode(params.userName, Utilities.Charset.UTF_8);
  const action = { toggle: 88, lock: 82, unlock: 83 }[params.action];

  if (isAuthed(params.password)) {
    UrlFetchApp.fetchAll(
      devices.map(device => ({
        url: `https://app.candyhouse.co/api/sesame2/${device.uuid}/cmd`,
        method : "post",
        payload: JSON.stringify({ cmd: action, history: encodedUserName, sign: generateCmacSign(device.secretKey) }),
        headers: { 'x-api-key': apiKey },
      }))
    );
  }

  return ContentService.createTextOutput(JSON.stringify({ result: 'ok' }));
}

function isAuthed(inputPassword) { 
  raw = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_512, inputPassword, Utilities.Charset.UTF_8);

  // https://qiita.com/tatataichi/items/7e3a27e1b69200d3fd86#%E3%82%B3%E3%83%BC%E3%83%89%E5%86%85%E5%AE%B9
  let inputPasswordDigest = ''; //ハッシュ値の文字列
  let numHash = 0;  //ハッシュ値の数値

  for (i = 0; i < raw.length; i++) {
    numHash = raw[i];
    if (numHash < 0) { //ゼロ未満だったら
      numHash += 256;  //マイナスの除去
    }
    if (numHash.toString(16).length == 1) { 
      inputPasswordDigest += '0'; //1桁なら0をくっつける
    }
    inputPasswordDigest += numHash.toString(16); //16進数表記の文字列に変換
  }

  return passwordDigest === inputPasswordDigest;
}

// https://zmzlz.blogspot.com/2021/06/sesame3-sesame-os2-gas-web-api.html
function generateCmacSign(secKey) {
  const date = Math.floor(Date.now() / 1000);
  const dateDate = new DataView(new ArrayBuffer(4));
  dateDate.setUint32(0, date, true);
  const msg = dateDate.getUint32(0).toString(16).slice(2, 8);  
  const hex = CryptoJS.enc.Hex.parse;
  return CryptoJS.CMAC(hex(secKey), hex(msg)).toString();
}

```

### 1-6. ウェブアプリとしてデプロイする
こんな感じで4つのファイルになっていればok
![スクリーンショット 2022-12-06 1.08.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/b3e61de0-85d7-28ed-fd3a-f525acf0d189.png)

あとは右上の「デプロイ」から「ウェブアプリ」としてデプロイする
![スクリーンショット 2022-12-08 21.44.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/ff864dfd-4612-ffe8-12d2-6b5a481748d0.png)
![スクリーンショット 2022-12-08 21.44.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/78091164-c38d-c1ac-de05-c8722a6506d0.png)
![スクリーンショット 2022-12-08 21.44.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/3801589d-d5b4-f6dd-d257-f023852cf406.png)
![スクリーンショット 2022-12-08 21.45.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/cf30baae-a4dc-c71b-4c8d-71e3b9a078e4.png)


デプロイ後に発行されるこんな感じのURLをメモっておく
`https://script.google.com/macros/s/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/exec`

## 2. `IFTTT → GAS` の設定
### 2-1. 右上の `Create` からAppletの作成
![スクリーンショット 2022-12-06 14.14.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/3160965c-f531-4172-9e23-7abb8228ac63.png)


### 2-2. `If This` を設定
![スクリーンショット 2022-12-06 10.57.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/8e48f40b-38cc-5cd3-8e8a-0a09e1ec4b78.png)

### 2-3. `Amazon Alexa` を選択
![スクリーンショット 2022-12-06 10.57.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/c3e9bbe7-c817-dce6-129e-5cc497885eab.png)

### 2-4. `Say a specific phrase` を選択
![スクリーンショット 2022-12-06 10.57.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/615f46ec-19a9-1d90-3415-c1310f78e09e.png)

### 2-5. Alexaアカウント連携して鍵を開ける時のフレーズを設定
![スクリーンショット 2022-12-06 10.58.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/afd18957-2479-7ca2-c706-e1529bd4f3b4.png)

### 2-6. `Then That`　を設定
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3233363937382f62626562626535662d353233662d333237612d386539632d6131303733643262666165322e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/ac2f48e1-c796-4402-1540-48fd8ec64e70.png)


### 2-7. `Webhooks` を選択
![スクリーンショット 2022-12-06 10.59.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/13005ea2-0d1a-17dc-e654-a3553963fc71.png)

### 2-8. `Make a web request` を選択
![スクリーンショット 2022-12-06 10.59.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/19d68ff1-db7c-e9e9-18a8-466b3ca590a7.png)

### 2-9. web requestの設定
![スクリーンショット 2022-12-09 0.11.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/8432e333-5552-1077-d95e-855c17258278.png)


- `URL` はGASをデプロイした時に発行されたURL (`https://script.google.com/macros/s/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/exec`)
- `Method` は `POST`
- `Content Type` は `application/json`
- `Body` は `{ "password": "oreno_password_daze", "userName": "alexa_ifttt", "action": "unlock" }`
  - `password` の値には `credentials.gs` に定義した `passwordDigest` の作成時に使った(ハッシュ化前の)文字列
  - `userName` はSESAMEのアプリの操作履歴に表示されるので自身がわかりやすい文字列を
  - `action` は今回は解錠が目的なので `unlock` で

こんな感じで `Create action`

# 2-10. あとは`Continue`, `Finish`で設定完了
![スクリーンショット 2022-12-08 21.53.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/723d573f-b89b-3817-9616-6b424e9ba0c6.png)
![スクリーンショット 2022-12-08 21.53.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/17694f1e-0382-2835-3d18-15dbbf20e69b.png)

`Applet Title` は適当に任意の名前で

## 3. `Alexa → IFTTT` の設定
### 3-1. アプリからAlexaの提携アクションを新規作成する
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/fb357ee4-66bc-bbe1-fe31-8dfce6d1a59d.jpeg" width="400">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/5e1deb2d-61d5-7890-5c8f-94eae6c2a5b1.jpeg" width="400">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/c6abc7c3-e7fd-00de-41a6-b256d15cc5ec.jpeg" width="400">

### 3-2. 定型アクション名を決める
ここの名前もお好みで適当に

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/b590a22b-c4e9-c98a-f0de-ecef188e4234.jpeg" width="400">

### 3-3. 実行条件を設定する
`音声` を選択してフレーズを決める

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/81a6bf0f-8e00-79d2-26ac-8ee1be994d14.jpeg" width="400">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/228eabfa-4d07-9af2-754f-ce384916fcb0.jpeg" width="400">

### 3-4. アクションを設定する
`IFTTT` を選択してさきほど作成したアプレットを選択する

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/264b4c89-5b8e-5045-4a33-a434a5d31c02.jpeg" width="400">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/a47f1a68-b15e-2ea0-e00e-c6a5a174e05c.jpeg" width="400">


こんな感じで設定したら保存する
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/a4fe7b00-903f-b50c-4c88-89cda2ecb48f.jpeg" width="400">

これでAlexa経由での解錠の問題は解決！
「アレクサ、解放してくれ！」の1回で鍵が2つとも開く！

# ドアが閉まったら鍵が閉まるようにする
## 1. [SwitchBot開閉センサ](https://amzn.asia/d/0qORq8m)と[SwithBot Hub Mini](https://amzn.asia/d/88Fwl0u)を購入・設置しAlexaと連携させる
Alexaとの連携はこちらの[公式の記事](https://support.switch-bot.com/hc/ja/articles/1500012761042-SwitchBot%E9%96%8B%E9%96%89%E3%82%BB%E3%83%B3%E3%82%B5%E3%83%BC%E3%81%A8Amazon%E3%82%A2%E3%83%AC%E3%82%AF%E3%82%B5%E3%81%AE%E9%80%A3%E6%90%BA%E6%96%B9%E6%B3%95)を参考に

## 2. SESAMEもAlexaと連携させる
こちらも[公式の記事](https://ameblo.jp/candyhouse-inc/entry-12684103055.html)を参考に

## 3. Alexaアプリで定型アクションを設定する
### 3-1. 定型アクション名を設定
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/5181aa06-c9da-6f94-2271-c05f2d5d46fe.jpeg" width="400">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/9e34b1bd-ef29-7ee5-fa83-bd757756856d.jpeg" width="400">

### 3-2. 実行条件を設定
開閉センサの「閉」をトリガーに設定する

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/d942a82a-7d59-8e04-51f5-c2ade784bf0e.jpeg" width="400">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/4c5dad14-6005-d7c1-c3a2-4847792a6040.jpeg" width="400">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/653dc574-0e38-29ca-589b-058daa121697.jpeg" width="400">

### 3-3. アクションを設定
SESAMEの施錠をアクションに設定する

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/c85390cc-9a99-34ec-a771-802964650ef8.jpeg" width="400">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/63eab132-25bd-db09-8c78-2ff2f7117647.jpeg" width="400">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/0bedf4f3-8f2a-d7a8-a9f2-9bb824b1c840.jpeg" width="400">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/af6a9f59-d4c0-2d98-2549-16f944c392e1.jpeg" width="400">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236978/c6f6584f-0ab2-13d5-a953-855e19c221d4.jpeg" width="400">


我が家は鍵が2つあるので施錠のアクションを2つ設定する

これで扉が閉まるとSESAMEも施錠されるようになりました！
便利便利


# これで完璧我が家のスマートロック
靴を履きながらAlexaに解錠を指示したり
家を出たら鍵が閉まるのを横目に確認しながらエレベータを呼んだり
SwitchBot開閉センサは開けっぱなしの時に通知したり、動体検知したら通知したりもできるので便利

#### SESAMEに限らず
APIが解放されてたりAlexaに対応してたりIFTTT対応しているスマートロックなら同じことできそう


# IFTTT、GAS経由で解錠できるようすることのリスクについて
- GASのソースには秘匿情報置いていないので流出しても問題がない
- Alexa経由での解錠時にパスコードが不要になったがフレーズをユニークにすることでリスクを抑えることできる
    - また、もともとパスコードを声で言う必要があるのでどっちにしても盗聴に対するリスクは変わらない
- Alexaをドアや窓の近くに置かないという定番の対策もしておくに越したことはない

## 万が一IFTTTにあるパスワードやスクリプトプロパティにある`API_KEY`などが流出した場合
- スクリプトプロパティにある`PASSWORD_DIGEST` を変更する
- [SESAMEの管理画面](https://partners.candyhouse.co/) から `API KEY` のリフレッシュする
- SESAME自体をリセットして `Secret Key` をリフレッシュする

**※ この仕組みで何かあっても責任は取れないので導入する際には自己責任でお願いします**