ESP32でドアの鍵モニター

# TL;DR
家の鍵の開け閉めををIFTTTに通知するデバイスをESP32で作ってみた。
ESP32は低電圧保護機能(Brownout reset)を無効にすれば、(非奨励だけど)2V付近でも動くので，単3電池2本とスイッチだけの単純な構成でIoT鍵監視装置が作れる。

# はじめに
スマートロック製品いろんなところから出てますよね．
個人的には家の鍵をリモートで開け閉めしたり出来なくて良くて，「家の鍵を締めたか」さえわかれば良いなって思ってたのでESP32でシンプルな鍵モニタリングシステムを作ってみました．

イメージしたシステム構成は以下の通り．
ESP32が鍵の状態をIFTTTに通知，IFTTTがESP32からの通知をスマートフォンに仲介する流れです．
(ほんとは通知用のアプリとか作ってみたいんですが，IFTTTの手軽さが勝ってしまう・・・)
<img width="400" alt="Concept" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145065/54a0175c-ba65-b8ba-96e2-1b0b04d3d112.png">

今回は，まずIFTTTでESP32からの通知をスマートフォンに中継する方法を説明し，その後ESP32でドアの鍵状態をIFTTTに通知する方法を説明します．

# IFTTTによる通知中継

基本的には，IFTTTの[Webhooks](https://ifttt.com/maker_webhooks)でESP32からのメッセージを受けて，[スマートフォンのIFTTTアプリ](https://ifttt.com/if_notifications)に転送する流れ．

Webhooksについての設定は，殆ど先人の知恵を丸パクリです．(先人の知恵：[IFTTT（イフト）でWebhooksの利用](https://www.miki-ie.com/network/webhooks-on-ifttt/))
URLを叩くことでトリガーが発火，JSONで引数を与えることも出来ます．
今回は，ESP32でURLを叩くときに，スマートフォンへ通知する文章を格納したJSONを付けておくことで，メッセージの内容等の設定をESP32のソースに集約して，IFTTTの機能を中継に限定しました．
また，叩くべきURLが何処にあるかが微妙にわかりづらいですが，[Webhooksのページ](https://ifttt.com/maker_webhooks)の右上"Documents"を押したページに表示されます．URLの確認やWebhooksの動作テストが出来るのでなかなか便利です．
<img width="800" alt="webhooks_setting" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145065/dd4446b0-a4ce-dffd-2b6f-f024f7d9abc3.png">
出力には，スマートフォンに入れた[IFTTTアプリに通知を飛ばせる](https://ifttt.com/if_notifications)ので，これを使います．
特に設定は必要ないはず?
IFTTT内の設定はこんな感じになるはずです．(縦に長いのなんとかならないのか)
<img width="200" alt="IFTTT_setting" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145065/26c4a569-de98-bbef-47cb-caedb66f6ba5.png">

IFTTT内で設定を済ませたら，上手く通知が飛ぶかを先ほどのWebhooksテストページで試しておくと，
何かトラブルがあったときの問題の切り分けがラクです．

# ESP32での鍵モニタリング

最近は素のM5St.+の方が人気がある気もしないでもないですが，シンプルさ優先でESP32-WROOM-32を使います．
WiFi使えるチップあるあるで電源回路泣かせなESP32ですが，どうやらNiMH電池2本(3V～2V)で動くようです[(『ESP32 電池（NiMH×2）で動かすときは 40MHz でコンパイルせよ 』)](http://wakwak-koba.hatenadiary.jp/entries/2017/02/28)．
と，言うことでNiMH電池2本を直結すれば動くと期待して電池駆動を目指します．
当然消費電力を絞っていきたいので，通常時はSleep状態にして，鍵の状態が変化したときだけ起動してWiFi経由で通知を飛ばすことにします．

単純すぎて悲しくなってきますが，回路構成は以下の通り．電池と鍵のノブで押されるスイッチ，ESP32の3つだけです．
I/Oは何処のピンでも良いんですが，Sleepからの復帰のトリガーになってもらうので，RTC_GPIOピン(GPIO0, 2, 4, 12~15, 25~27, 32~35)&&特に他に役割がないピンである必要があります．今回はGPIO2を使ってます．
ちなみに，プログラムの開発自体は[ESP32-Devkit-C](http://akizukidenshi.com/catalog/g/gM-11819/)でやりましたが，最終的には余計なチップの[載っていないボード](https://www.switch-science.com/catalog/3170/)で運用しています．
<img width="200" alt="schematic" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145065/f003d35f-c6f9-a1de-2bc1-8a58e0707ed8.png">

書き込んだスケッチは長いので記事の最後に貼ってあります．
起きる度にスイッチの状態が変わったかを確認して，変わっていたら変更内容に応じたメッセージをJSONに格納してIFTTTに向かって投げます．
また，IFTTTに投げ終わったら，次の復帰条件をセットしてsleepモードに入ります．
IFTTTにJSONを投げる件は[こちら(『M5Stick-CでJsonをPOSTする』)](https://qiita.com/poruruba/items/4bf6a52520e431a8f4a5)の記事を参考にしました．

**また，ESP32のArduino開発環境だと，電源電圧が2.4V以下になるとリセットがかかるような初期設定になっています．**

```cpp
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // disable brownout detector
```
を実行してBrownout detecterによる低電圧保護を無効にしないと，電池容量の半分も使えなくなってしまいます．
**あと前述の[ブログ](http://wakwak-koba.hatenadiary.jp/entries/2017/02/28)に書いてあるように，SPI Flashの動作周波数を40 MHzに切り替えるのを忘れずに．**
スイッチを押したり離したりして，通知が飛ぶことを確認したらいよいよ設置です．

# 設置＆まとめ
<img width="400" alt="door" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145065/e2138138-3206-30bf-dbde-656c3a54ae41.jpeg">
鍵のレバーが真っ平らだったので，アクリルの板を張り付けて鍵の開け締めでマイクロスイッチが押されるようにしてみました．
こんな感じでスイッチと電池，ESP32を扉に貼り付けて運用しています．
多分防滴とかのことを考えると，スイッチをリードスイッチにして全部タッパーの中にしまうとかした方が良いんですが，単純さを優先してこういう構成にしてあります．
まだ運用を始めて一週間たっていないので電池がどれだけ持つのかわかりませんが，しばらくこれで様子を見てみようと思います．

# おまけ: ESP32の動作電圧
今回はNiMH2本で動かしていますが，実は一般にESP32と呼ばれてるESP32-WROOM-32は[データシート](https://www.espressif.com/sites/default/files/documentation/esp32-wroom-32_datasheet_en.pdf)を見ると，
> Operating voltage/Power supply: 3.0V ~ 3.6V

との記載があります.
NiMHが1本あたり満充電1.5Vで終端1Vぐらいなので，2本で3.0V～2.0V全然足りてないですね:thinking:
ESP32-WROOM-32はESP32とSPI Flash，アンテナ等々を纏めたチップですが，[ESP32単体のデータシート](https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf)を見てみると以下のように書いてあります．

>Recommended Operating Conditions: 2.3V ~ 3.6V

と，言うことでESP32自体は2.3Vでもイケるご様子．まぁ，趣味に使う分には多少無理させても良いでしょう・・・DC/DC高いもんね．
ちなみに，ESP32-WROOM-32のデータシートのRevision historyを見てみると元々もっと低い電圧で動く表記だったのが，だんだん引き上げられて現在の値になっているみたいです．

# ESP32のソースコード

```cpp:DoorLockMonitor_ESP32.ino
#include <ArduinoJson.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>

#include "MyAPSettings.h" // Contain following 3 constants.
// const char *SSID_AP = "hogehoge";
// const char *PASSWORD_AP = "hogehoge";
// const char *IFTTT_URL = "http://maker.ifttt.com/trigger/...";

const gpio_num_t DOOR_LOCK_SW_PIN = GPIO_NUM_2;

enum DoorLockStatus { LOCKED, UNLOCKED, INVALID };
RTC_DATA_ATTR DoorLockStatus last_status =
    INVALID; // Keep last status in RTC SLOW memory.

void setup() {
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // disable brownout detector
  Serial.begin(115200);
  pinMode(DOOR_LOCK_SW_PIN, INPUT_PULLUP);
}
void loop() {
  Serial.printf("Last status: %s.\n",
                DoorLockStatusToString(last_status).c_str());
  DoorLockStatus current_status = GetDoorLockStatus();
  Serial.printf("Current status: %s.\n",
                DoorLockStatusToString(current_status).c_str());
  if (last_status == current_status) {
    Serial.println("Enter deep sleep mode.");
    esp_sleep_enable_ext0_wakeup(
        DOOR_LOCK_SW_PIN,
        digitalRead(DOOR_LOCK_SW_PIN) == HIGH
            ? LOW
            : HIGH); // Wake up by any change on DOOR_LOCK_SW_PIN;
    esp_deep_sleep_start();
  }
  PostIFTTT(String("Door ") + DoorLockStatusToString(current_status));
  last_status = current_status;
  delay(1000);
}

String DoorLockStatusToString(DoorLockStatus status) {
  switch (status) {
  case LOCKED:
    return "Locked";
  case UNLOCKED:
    return "Unlocked";
  default:
    return "Invalid";
  }
}
DoorLockStatus GetDoorLockStatus() {
  if (digitalRead(DOOR_LOCK_SW_PIN) == HIGH) {
    return LOCKED;
  }
  return UNLOCKED;
}

void ConnectWiFi() {
  if (WiFi.status() == WL_CONNECTED) {
    return;
  }
  WiFi.mode(WIFI_STA);
  WiFi.begin(SSID_AP, PASSWORD_AP);
  for (int retry_count = 0; retry_count < 10; retry_count++) {
    if (WiFi.status() == WL_CONNECTED) {
      break;
    }
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
}
void PostIFTTT(String str) {
  ConnectWiFi();
  Serial.println("WiFi connected");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  StaticJsonDocument<JSON_OBJECT_SIZE(1)> json_request;

  json_request["value1"] = str.c_str();

  char buffer[255];
  serializeJson(json_request, buffer, sizeof(buffer));
  Serial.println(buffer);
  HTTPClient http;

  http.begin(IFTTT_URL);
  http.addHeader("Content-Type", "application/json");
  http.POST((uint8_t *)buffer, strlen(buffer));
  http.end();
}
```