「セサミ」をNFCタグとNode-REDで操作 (Sesame API サブフローを作りました)

#はじめに
過去に腰痛で救急車で病院に運ばれた事があったのですが、独りだと救急車を呼んでも動けなくて玄関の鍵を開けられないんですよね。
また、将来親の介護においても鍵の遠隔操作は必要となってくるので、スマートロックがとりあえずどのようなものなのか、[CANDY HOUSE](https://jp.candyhouse.co/) 様から発売されている「セサミ」を購入しました。

この製品は、オプションのWifiアクセスポイントを使う事で、REST APIによる操作が可能となります。
内部的な仕組みは、WifiアクセスポイントがCANDY HOUSEのサーバからAPIを受信し、Bluetoothでセサミと通信するだけのようです。
ですが、私は運悪くBluetoothの通信距離が30cmくらいしかないWifiアクセスポイントの不具合品に当たってしまって無償交換しています。モバイルバッテリーにWifiアクセスポイントを接続して徐々にセサミ本体との距離を縮めた結果、不具合部位を切り分けできました。

実は、公式から[NFCリーダー連携の記事](https://ameblo.jp/candyhouse-inc/entry-12413908790.html)が出ており、たまたま機材も持っているのですが、正直なところ気軽にできるような内容ではないので、もっと敷居の低い「NFCタグ」によるWebhookでの連携を考えてみました。
結構気軽に使えて良い感じです。

#前提環境
- Androidスマートフォン
    - Google Playより「[Termux](https://play.google.com/store/apps/details?id=com.termux&hl=ja)」をインストール後、[Node-REDをインストール](https://nodered.jp/docs/platforms/android)済み (Node-RED v1.0.2)
- セサミ スマートロック本体 + Wi-Fiアクセスポイント
    - [CANDY HOUSE Dashboard](https://my.candyhouse.co/#/)で、APIキー発行済み
- 書き込みできる安いNFCタグ

画面のタップ操作ではNode-REDの操作が完全にできないので、マウスは必須だと思います。スマートフォンにマウスを接続して頑張るでも良いのですが、自宅などのプライベートなwifiに接続した状態で、AndroidのIPアドレスを調べて、同じネットワークに接続しているパソコンからNode-REDを操作した方が楽です。

スマートフォンのIPアドレスは、Android OS上からは「設定」メニューから確認できます。
Termux上でも、「`ifconfig`」や「`ip addr show`」(略せば「`ip a`」)で確認できます。

```shell-session:termux
$ ip a
     :(略)
26: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 3000
    link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.19/24 brd 192.168.1.255 scope global wlan0
     :(略)
```

確認したIPアドレスで、パソコンのブラウザから接続すれば、スマートフォンのNode-RED画面が開きます。
http://スマートフォンのIPアドレス:1880
![スクリーンショット 2020-10-12 12.32.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/2ce13d36-f18a-107e-3d58-ddd0a24256f0.png)

#Node-REDでのフロー作成
フローはサブフローを作り、メインフローは以下のように作りました。
軽く説明すると、HTTPノードで「/open_sesame」にGET要求があれば、セサミの現在のロック状態と相反する状態にするコマンドを発行するフローです。

![スクリーンショット 2020-10-12 12.47.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/a1098d01-84d0-a90e-20ab-3034b466a3e0.png)

サブフローは以下のようになってます。

![スクリーンショット 2020-10-12 13.42.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/0a665d00-8009-5ba1-3610-c2b085d55daa.png)


フローを書き出したコードは以下です。※サブフローの環境変数設定を使っているので、Node-REDはv0.20以上が前提となってます。

```json:
[{"id":"6ca761f2.14ea","type":"subflow","name":"Sesame API","info":"","category":"","in":[{"x":30,"y":80,"wires":[{"id":"a132420d.88c9b"}]}],"out":[{"x":1200,"y":80,"wires":[{"id":"36020a99.6d9e06","port":0}]}],"env":[{"name":"URL","type":"str","value":"https://api.candyhouse.co/public","ui":{"icon":"font-awesome/fa-cloud","label":{"en-US":"Endpoint"},"type":"input","opts":{"types":["str"]}}},{"name":"KEY","type":"str","value":"","ui":{"icon":"font-awesome/fa-key","label":{"en-US":"API key"},"type":"input","opts":{"types":["str"]}}},{"name":"REQ","type":"str","value":"","ui":{"icon":"font-awesome/fa-sign-out","label":{"en-US":"Request"},"type":"select","opts":{"opts":[{"l":{"en-US":"Lock"},"v":"LOCK"},{"l":{"en-US":"Unlock"},"v":"UNLOCK"},{"l":{"en-US":"Sync"},"v":"SYNC"},{"l":{"en-US":"Status"},"v":"STATUS"}]}}},{"name":"DEV","type":"str","value":"","ui":{"icon":"font-awesome/fa-unlock-alt","label":{"en-US":"Device"},"type":"input","opts":{"types":["str"]}}}],"color":"#3FADB5","icon":"font-awesome/fa-cloud"},{"id":"4d4f04db.e897ac","type":"http request","z":"6ca761f2.14ea","name":"api.candyhouse.co","method":"use","ret":"obj","paytoqs":true,"url":"","tls":"","persist":false,"proxy":"","authType":"","x":510,"y":80,"wires":[["5619992b.e79fb8"]]},{"id":"6d7f0375.6d124c","type":"function","z":"6ca761f2.14ea","name":"Action","func":"\n// 指定したニックネームのセサミのインデックス番号を探す\nfor(var i=0;i<msg.devices.length;i++){\n    if(msg.devices[i].nickname === env.get(\"DEV\")) break;\n}\n\n// 指定したニックネームのセサミが見つからない場合は最初のセサミを操作する\nif(i === msg.devices.length) i = 0;\n\nmsg.headers = {};\nmsg.headers['Authorization'] = env.get(\"KEY\");\nmsg.headers['Connection'] = \"close\";\n\nswitch(env.get(\"REQ\")){\n    case \"STATUS\":\n        msg.method = \"GET\";\n        msg.url = env.get(\"URL\") + \"/sesame/\" + msg.devices[i].device_id;\n        break;\n    case \"LOCK\":\n        msg.method = \"POST\";\n        msg.url = env.get(\"URL\") + \"/sesame/\" + msg.devices[i].device_id;\n        msg.headers['Content-Type'] = 'application/json';\n        msg.payload = '{\"command\":\"lock\"}';\n        break;\n    case \"UNLOCK\":\n        msg.method = \"POST\";\n        msg.url = env.get(\"URL\") + \"/sesame/\" + msg.devices[i].device_id;\n        msg.headers['Content-Type'] = 'application/json';\n        msg.payload = '{\"command\":\"unlock\"}';\n        break;\n    case \"SYNC\":\n        msg.method = \"POST\";\n        msg.url = env.get(\"URL\") + \"/sesame/\" + msg.devices[i].device_id;\n        msg.headers['Content-Type'] = 'application/json';\n        msg.payload = '{\"command\":\"sync\"}';\n        break;\n}\n\n\n\nreturn msg;","outputs":1,"noerr":0,"x":890,"y":80,"wires":[["36020a99.6d9e06"]]},{"id":"aef8271d.e16938","type":"function","z":"6ca761f2.14ea","name":"Get device list","func":"\nmsg.headers = {};\nmsg.headers['Authorization'] = env.get(\"KEY\");\nmsg.headers['Connection'] = \"close\";\nmsg.method = \"GET\";\nmsg.url = env.get(\"URL\") + \"/sesames\";\n\nreturn msg;","outputs":1,"noerr":0,"x":310,"y":80,"wires":[["4d4f04db.e897ac"]]},{"id":"5619992b.e79fb8","type":"change","z":"6ca761f2.14ea","name":"","rules":[{"t":"move","p":"payload","pt":"msg","to":"devices","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":720,"y":80,"wires":[["6d7f0375.6d124c"]]},{"id":"36020a99.6d9e06","type":"http request","z":"6ca761f2.14ea","name":"api.candyhouse.co","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":1060,"y":80,"wires":[[]]},{"id":"a132420d.88c9b","type":"change","z":"6ca761f2.14ea","name":"initialize","rules":[{"t":"delete","p":"payload","pt":"msg"},{"t":"delete","p":"headers","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":140,"y":80,"wires":[["aef8271d.e16938"]]},{"id":"f5f02949.6e3118","type":"comment","z":"82a7a644.2aaca8","name":"セサミ施錠・解錠","info":"","x":110,"y":50,"wires":[]},{"id":"bbd9c2f0.9b28","type":"http in","z":"82a7a644.2aaca8","name":"","url":"/open_sesame","method":"get","upload":false,"swaggerDoc":"","x":130,"y":110,"wires":[["e9cb1a8.bc968e8"]]},{"id":"66a08451.abf11c","type":"http response","z":"82a7a644.2aaca8","name":"","statusCode":"200","headers":{},"x":980,"y":110,"wires":[]},{"id":"e9cb1a8.bc968e8","type":"subflow:6ca761f2.14ea","z":"82a7a644.2aaca8","name":"Status","env":[{"name":"KEY","type":"cred"},{"name":"REQ","value":"STATUS","type":"str"}],"x":350,"y":110,"wires":[["50f17c72.4a09b4"]]},{"id":"50f17c72.4a09b4","type":"switch","z":"82a7a644.2aaca8","name":"locked ?","property":"payload.locked","propertyType":"msg","rules":[{"t":"true"},{"t":"false"}],"checkall":"false","repair":false,"outputs":2,"x":490,"y":110,"wires":[["d3bdf3de.ac4d6"],["f048e0a0.0368b"]]},{"id":"6c0c9465.56e08c","type":"template","z":"82a7a644.2aaca8","name":"html","field":"payload","fieldType":"msg","format":"html","syntax":"mustache","template":"<!DOCTYPE html>\n<html>\n<script type=\"text/javascript\">\n<!--\nwindow.open('about:blank','_self').close();\n-->\n</script>\n</html>","output":"str","x":840,"y":110,"wires":[["66a08451.abf11c"]]},{"id":"d3bdf3de.ac4d6","type":"subflow:6ca761f2.14ea","z":"82a7a644.2aaca8","name":"Unlock","env":[{"name":"KEY","type":"cred"},{"name":"REQ","value":"UNLOCK","type":"str"}],"x":670,"y":90,"wires":[["6c0c9465.56e08c"]]},{"id":"f048e0a0.0368b","type":"subflow:6ca761f2.14ea","z":"82a7a644.2aaca8","name":"Lock","env":[{"name":"KEY","type":"cred"},{"name":"REQ","value":"LOCK","type":"str"}],"x":670,"y":130,"wires":[["6c0c9465.56e08c"]]}]
```

サブフローを呼び出す3つのノードを各々ダブルクリックして、「サブフローインスタンスを編集」を表示します。

![スクリーンショット 2020-10-12 15.10.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/53ad8826-c8e9-5863-9812-e05fcd0ca391.png)

「API key」に[CANDY HOUSE Dashboard](https://my.candyhouse.co/#/)で発行したAPIキーと、「Device」にセサミに付けたニックネームを入力してください。
「Device」のニックネームが間違っていたり、空の場合はデバイス一覧で取得した一番最初のセサミを操作します。つまり、1つしかセサミを持っていない場合は「Device」を入力しなくても操作できます。

![スクリーンショット 2020-10-12 13.55.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/4e4e2377-cba2-917c-93d0-f9741f510e0f.png)

インジェクトノードなどで動作確認してみてください。

#「Sesame API」サブフロー
標準ノード(Node-REDインストール後に使えるノード)のみでセサミAPIを呼び出すサブフローを作っています。
このサブフローでかなり楽にセサミのAPIを実行できます。

![スクリーンショット 2020-10-13 10.08.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/9bafdba0-1424-5428-491c-065d3b7f4672.png)

サブフローだけのJSONは以下となります。

```json:
[{"id":"331fddfb.2b61e2","type":"subflow","name":"Sesame API","info":"","category":"","in":[{"x":30,"y":80,"wires":[{"id":"36a1df93.8a455"}]}],"out":[{"x":1200,"y":80,"wires":[{"id":"5b05cd2f.346fd4","port":0}]}],"env":[{"name":"URL","type":"str","value":"https://api.candyhouse.co/public","ui":{"icon":"font-awesome/fa-cloud","label":{"en-US":"Endpoint"},"type":"input","opts":{"types":["str"]}}},{"name":"KEY","type":"str","value":"","ui":{"icon":"font-awesome/fa-key","label":{"en-US":"API key"},"type":"input","opts":{"types":["str"]}}},{"name":"REQ","type":"str","value":"","ui":{"icon":"font-awesome/fa-sign-out","label":{"en-US":"Request"},"type":"select","opts":{"opts":[{"l":{"en-US":"Lock"},"v":"LOCK"},{"l":{"en-US":"Unlock"},"v":"UNLOCK"},{"l":{"en-US":"Sync"},"v":"SYNC"},{"l":{"en-US":"Status"},"v":"STATUS"}]}}},{"name":"DEV","type":"str","value":"","ui":{"icon":"font-awesome/fa-unlock-alt","label":{"en-US":"Device"},"type":"input","opts":{"types":["str"]}}}],"color":"#3FADB5","icon":"font-awesome/fa-cloud"},{"id":"266f8c52.64b584","type":"http request","z":"331fddfb.2b61e2","name":"api.candyhouse.co","method":"use","ret":"obj","paytoqs":true,"url":"","tls":"","persist":false,"proxy":"","authType":"","x":510,"y":80,"wires":[["a4c41ed7.7421f"]]},{"id":"6269ff95.ca699","type":"function","z":"331fddfb.2b61e2","name":"Action","func":"\n// 指定したニックネームのセサミのインデックス番号を探す\nfor(var i=0;i<msg.devices.length;i++){\n    if(msg.devices[i].nickname === env.get(\"DEV\")) break;\n}\n\n// 指定したニックネームのセサミが見つからない場合は最初のセサミを操作する\nif(i === msg.devices.length) i = 0;\n\nmsg.headers = {};\nmsg.headers['Authorization'] = env.get(\"KEY\");\nmsg.headers['Connection'] = \"close\";\n\nswitch(env.get(\"REQ\")){\n    case \"STATUS\":\n        msg.method = \"GET\";\n        msg.url = env.get(\"URL\") + \"/sesame/\" + msg.devices[i].device_id;\n        break;\n    case \"LOCK\":\n        msg.method = \"POST\";\n        msg.url = env.get(\"URL\") + \"/sesame/\" + msg.devices[i].device_id;\n        msg.headers['Content-Type'] = 'application/json';\n        msg.payload = '{\"command\":\"lock\"}';\n        break;\n    case \"UNLOCK\":\n        msg.method = \"POST\";\n        msg.url = env.get(\"URL\") + \"/sesame/\" + msg.devices[i].device_id;\n        msg.headers['Content-Type'] = 'application/json';\n        msg.payload = '{\"command\":\"unlock\"}';\n        break;\n    case \"SYNC\":\n        msg.method = \"POST\";\n        msg.url = env.get(\"URL\") + \"/sesame/\" + msg.devices[i].device_id;\n        msg.headers['Content-Type'] = 'application/json';\n        msg.payload = '{\"command\":\"sync\"}';\n        break;\n}\n\n\n\nreturn msg;","outputs":1,"noerr":0,"x":890,"y":80,"wires":[["5b05cd2f.346fd4"]]},{"id":"11ff62ac.e0677d","type":"function","z":"331fddfb.2b61e2","name":"Get device list","func":"\nmsg.headers = {};\nmsg.headers['Authorization'] = env.get(\"KEY\");\nmsg.headers['Connection'] = \"close\";\nmsg.method = \"GET\";\nmsg.url = env.get(\"URL\") + \"/sesames\";\n\nreturn msg;","outputs":1,"noerr":0,"x":310,"y":80,"wires":[["266f8c52.64b584"]]},{"id":"a4c41ed7.7421f","type":"change","z":"331fddfb.2b61e2","name":"","rules":[{"t":"move","p":"payload","pt":"msg","to":"devices","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":720,"y":80,"wires":[["6269ff95.ca699"]]},{"id":"5b05cd2f.346fd4","type":"http request","z":"331fddfb.2b61e2","name":"api.candyhouse.co","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":1060,"y":80,"wires":[[]]},{"id":"36a1df93.8a455","type":"change","z":"331fddfb.2b61e2","name":"initialize","rules":[{"t":"delete","p":"payload","pt":"msg"},{"t":"delete","p":"headers","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":140,"y":80,"wires":[["11ff62ac.e0677d"]]},{"id":"a228c4aa.81a658","type":"subflow:331fddfb.2b61e2","z":"c33d685b.315298","name":"","env":[{"name":"REQ","value":null,"type":"str"}],"x":810,"y":180,"wires":[[]]}]
```

ノード(サブフロー)のダブルクリックでプロパティを開いて、各項目を埋めてご利用ください。

![スクリーンショット 2020-10-13 10.04.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/0776655b-6943-1560-2dbd-45e903bfce02.png)

| 項目 | 入力値 |
|:-:|:-:|
| 名前  | ノードに表示する文字列を入力します。<br>「施錠」とか「解錠」とかわかりやすい文字列が良いと思います。  |
| Endpoint  | セサミAPIのエンドポイントURLです。<br>当面「[https://api.candyhouse.co/public](https://api.candyhouse.co/public)」固定になると思います。  |
| API key  | セサミに登録したご自身のアカウントで発行したAPIキーを入力します。<br>伏せ文字「＊」にしたかったのですが、不具合があり無理のようです。  |
| Request  | 「Lock」「Unlock」「Sync」「Status」から選んで設定します。  |
| Device  | セサミに付けた(恥ずかしい)ニックネームを入力します。  |

「Device」のニックネームが間違っていたり、空の場合は、セサミAPIのデバイス一覧取得した際に返るJSON配列のインデックス「0」のセサミを操作します。つまり、1つしかセサミを持っていない場合は「Device」を入力しなくても操作できます。

１台のセサミしか持っていないので、複数台の場合の動作検証は全くしておりません！

#NFCタグにURLを書き込む
Amazonでずいぶん昔に購入したのですが、使用したNFCタグは以下の画像と同じものです。
![Free-Sample-Printable-13-56MHZ-NTAG213-RFID.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/7cf68151-e120-5ebc-cf43-a8d70221f1ba.jpeg)

書き込みは、Google PlayからNFCタグの書き込みアプリをインストールします。
今回私は「[NFC TagWriter by NXP]
(https://play.google.com/store/apps/details?id=com.nxp.nfc.tagwriter&hl=ja)」というアプリを使いました。
書き込む内容は、Node-RED側の待ち受けURLで「`http://127.0.0.1:1880/open_sesame`」とします。

![Screenshot_20190506-060333.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/175ad0a9-59ac-9a9e-987e-cbcd6079c5b9.png)

NFCタグにURLを書き込んだら、スマートフォンをかざしてブラウザが起動するか動作確認をしてください。画面ロックを解除しないとNFCリーダーが動作しないので、画面のロック解除を忘れずにしてください。
動作確認できたら、タグ情報をロックして、ドアの内側や外側に貼りましょう。注意点としては、金属の上に貼らないようにしてください。金属の上だと、動作しなかったり認識がとても鈍くなります。

ちなみに、ドアの外にNFCタグを貼ったとしても、書き込まれているURLのアドレスは自分自身を表すローカル・ループバックアドレス（127.0.0.1）ですので、他人のスマホでは意味のない情報となります。

#おわりに
実際の使用感はこんな感じです。

![open_sesame.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/71598a3e-7663-b5e9-dade-7e97f1984cff.gif)

REST APIでセサミを操作する場合、どうしてもタイムラグが発生するのですが、出かける際の施錠でもドアの内側のNFCタグにかざしてドアを閉める余裕があるので、ある意味「ちょうど良い感じ」に使えています。
帰宅時はキーホルダータイプのNFCタグにかざして解錠しています。

Node-REDのサブフローが進化しており、自作ノードのように使えるようになっていました。
正直なところ、追加ノードはバージョンアップ後に大きく仕様が変わったりして、何度か大変な思いをした事もあり、個人的には標準ノードの組み合わせでサブフローを作った方が安心できます。
追加ノードなど、Node-REDの拡張部分が今後どうなっていくのかはわかりませんが、やり方は色々あった方が良いと思ってます。