トイレの空き状況をチャットツールから確認できるようにしてみた。これってIoT！？

# はじまり

むかしむかし、IoTというバズワードと共に、トイレ運用アプリが流行った時期がありました。
もう何番煎じかもわからないけど、負けじと作ってみたものをここに公開します。

# 仕組み

私が足繁く通っている会社では、東日本大震災の教訓を忘れずに節電の意識を保っていることから、基本的にトイレの照明はその都度消しているという運用です。
と、いうことで、空き状況のチェックは光センサーひとつでOKでした。

![20160402235311.png](https://qiita-image-store.s3.amazonaws.com/0/131050/e6e15291-c9e2-3493-a7c6-792cc80f37b9.png)

# トイレ

ESP-WROOM-02は、スイッチサイエンスで開発ボードを購入しました。
選定理由は、全部載せで楽だなと思ったので。

## 主な部品

| 商品名 | 価格 |
|:--|--:|
| ESP-WROOM-02開発ボード | 2,160円 |
| TSL2561デジタル光センサボード | 702円 |

# AWS

当初は、AWS IoTを使ってみたかった。
どうやら、ESP-WROOM-02ではTLS1.2に対応するのが難しいらしい、ということで断念しました。
で、EC2にMosquittoをインストールしました。 Hubotをインストールしなければならないことから、どちらにしろ別途サーバは必要だったかな。
特にEC2である必然性はなく、スペックも最低限のものでOKです。

# Hubot #1

https://github.com/kunikada/hubot-toilet
Hubotのスクリプト。こんなことやります。

* Mosquittoにトイレ使用状況の問い合わせ
* チャットの返事、連絡など
* センサーデバイスの細かなパラメータの設定

# Hubot #2

https://github.com/kunikada/hubot-chatwork
ChatWorkアダプタ

おそらく、みんなが使っているであろう、ググったら出てくるChatWorkアダプタに若干手を入れました。
ルームの指定をしなくてもいいようにしています。

# チャット

うちの会社では、ChatWorkを使用しています。正直APIは使いにくいです。
専用アカウントを用意して、APIの使用申請をしています。
このあたりは、Slackを使用するのであれば、HubotのアダプタをSlackに変えればいいので置き換え可能です。

# 使用イメージ

![20160403005246.jpg](https://qiita-image-store.s3.amazonaws.com/0/131050/3e4681e1-a193-e8cf-d0ae-233dcf2791ff.jpeg)

![20160403012942.png](https://qiita-image-store.s3.amazonaws.com/0/131050/55544c61-39f0-d920-5eb8-fb42119c81a7.png)


# 感想

当初、設置の自由度のために、乾電池やバッテリー駆動を目指してみましたが、思ったより電力を消費しています。
コンセントが近くにあるなら、素直にそこから電源をとったほうが良さそうです。
電池でやるとしたら、パーツ構成とプログラムの見直しが必要そうです。

# ソースコード

```c:sketch.ino
#include <Wire.h>
#include <Ethernet.h>
#include <ESP8266WiFi.h>
#include <NTPClient.h>
#include <PubSubClient.h>
#include <TSL2561.h>

#define WIFI_SSID "YOUR_WIFI_SSID"
#define WIFI_PASS "YOUR_WIFI_PASSWORD"

IPAddress mqttHost(123, 45, 67, 89);
#define MQTT_PORT 1883
#define MQTT_USER "YOUR_MQTT_USER"
#define MQTT_PASS "YOUR_MQTT_PASSWORD"

#define TSL2561_VDD 2
#define TSL2561_GND 15
#define TSL2561_SDA 13
#define TSL2561_SCL 12
#define TSL2561_ADDR 0x39

void callback(char*, byte*, unsigned int);
void onCheck();
void onChange(int, int);

WiFiClient wifiClient;
NTPClient timeClient("ntp.jst.mfeed.ad.jp");
PubSubClient mqttClient(mqttHost, MQTT_PORT, callback, wifiClient);
String clientName;
int cntPub = 0;
int cntChange = 0;

TSL2561 tsl(TSL2561_ADDR);
int progress, lastResult;
int luxThreshold = 50;
int sleepMinOnCheck = 10;
int sleepMinOnChange = 0;
bool sleepHour[24] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
bool sleepDow[7] = {0,0,0,0,0,0,0};

void setup() {
  Serial.begin(115200);
  
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(".");
  }
  Serial.println("connected");

  while (timeClient.getRawTime() < 60) { // 1 minute timeout
    timeClient.update();
    delay(2000);
  }

  // 識別子としてMACアドレスを使用
  uint8_t mac[6];
  WiFi.macAddress(mac);
  for (int i = 0; i < 6; i++) {
    clientName += String(mac[i], HEX);
  }

  // MQTTサーバから設定を取得
  mqttClient.connect(clientName.c_str(), MQTT_USER, MQTT_PASS);
  String topic = "reset/" + clientName;
  String payload = String(timeClient.getRawTime());
  mqttClient.publish(topic.c_str(), payload.c_str(), true);
  topic = clientName + "/settings/lux_threshold";
  mqttClient.subscribe(topic.c_str(), 1);
  topic = clientName + "/settings/sleepmin_oncheck";
  mqttClient.subscribe(topic.c_str(), 1);
  topic = clientName + "/settings/sleepmin_onchange";
  mqttClient.subscribe(topic.c_str(), 1);
  topic = clientName + "/settings/sleep_hour";
  mqttClient.subscribe(topic.c_str(), 1);
  topic = clientName + "/settings/sleep_dow";
  mqttClient.subscribe(topic.c_str(), 1);
  for (int i = 0; i < 15; i++) {
    if (cntPub == 5) {
      Serial.println("settings loaded");
      break;
    }
    mqttClient.loop();
    delay(1000);
    Serial.print(".");
  }
  mqttClient.disconnect();

  // ピン設定
  pinMode(TSL2561_VDD, OUTPUT);
  pinMode(TSL2561_GND, OUTPUT);
  digitalWrite(TSL2561_VDD, HIGH);
  digitalWrite(TSL2561_GND, LOW);
  Wire.begin(TSL2561_SDA, TSL2561_SCL);
 
  // 周囲の明暗状況に合わせて適切にゲインを変更してください
  //tsl.setGain(TSL2561_GAIN_0X);         // ゲインなし:周囲が明るい場合
  tsl.setGain(TSL2561_GAIN_16X);      // ゲインx16:周囲が暗い場合

  // 積算時間を変更することで、光の測定時間を変更することができます
  // 長時間測定すると、データの取得は遅くなりますが、低光度環境での測定性能が向上します
  tsl.setTiming(TSL2561_INTEGRATIONTIME_13MS);  // 短時間測定:明るい環境
  //tsl.setTiming(TSL2561_INTEGRATIONTIME_101MS);  // 中時間測定:中程度の明るさ
  //tsl.setTiming(TSL2561_INTEGRATIONTIME_402MS);  // 最長時間測定:暗い環境
}

void loop() {
  onCheck();

  // 明るさを取得
  uint32_t lum = tsl.getFullLuminosity();
  uint16_t ir = lum >> 16;
  uint16_t full = lum & 0xFFFF;
  uint32_t lux = tsl.calculateLux(full, ir);

  // 同じ状態が5回連続した場合にステータスを変更  
  int result = 0;
  progress = progress << 1;
  if (lux > luxThreshold) {
    progress++;
  }
  progress &= 0x1f;
  if (progress == 0x1f) {
    result = 1;
  } else if (progress == 0) {
    result = 2; 
  }

  if (result > 0 && result != lastResult) {
    onChange(result, lux);
  }
  
  delay(1000);
}

void callback (char* topic, byte* payload, unsigned int length) {
  String strTopic = String(topic);
  String s = String((char*) payload).substring(0, length);
  strTopic.replace(clientName, "");
  if (strTopic == "/settings/lux_threshold") {
    luxThreshold = atoi(s.c_str());
  } else if (strTopic == "/settings/sleepmin_oncheck") {
    sleepMinOnCheck = atoi(s.c_str());
  } else if (strTopic == "/settings/sleepmin_onchange") {
    sleepMinOnChange = atoi(s.c_str());
  } else if (strTopic == "/settings/sleep_hour") {
    char h[2];
    for (int i = 0; i < 24; i++) {
      sprintf(h, "%02d", i);
      if (s.indexOf(h) != -1) {
        sleepHour[i] = 1;
      }
    }
  } else if (strTopic == "/settings/sleep_dow") {
    s.toLowerCase();
    if (s.indexOf("sun") != -1) { 
      sleepDow[0] = 1;
    }  
    if (s.indexOf("mon") != -1) { 
      sleepDow[1] = 1;
    }  
    if (s.indexOf("tue") != -1) { 
      sleepDow[2] = 1;
    }  
    if (s.indexOf("wed") != -1) { 
      sleepDow[3] = 1;
    }  
    if (s.indexOf("thu") != -1) { 
      sleepDow[4] = 1;
    }  
    if (s.indexOf("fri") != -1) { 
      sleepDow[5] = 1;
    }  
    if (s.indexOf("sat") != -1) { 
      sleepDow[6] = 1;
    }  
  }
  Serial.println(topic);
  Serial.write(payload, length);
  Serial.println("");
  cntPub++;
}
  
void onCheck() {
  int dow = ((timeClient.getRawTime() + 32400) / 86400L + 4) % 7; 
  int hour = (timeClient.getRawTime() % 86400L / 3600 + 9) % 24;
  if (sleepDow[dow] || sleepHour[hour]) {
    ESP.deepSleep(sleepMinOnCheck * 60 * 1000 * 1000);
    delay(1000);
  }
}

void onChange(int result, int lux) {
  mqttClient.connect(clientName.c_str(), MQTT_USER, MQTT_PASS);
  String topic = clientName + "/result";
  String payload = String(timeClient.getRawTime()) + " " + String(result) + " " + String(lux);
  while (!mqttClient.publish(topic.c_str(), payload.c_str(), true)) {
    delay(1000);
  }
  mqttClient.disconnect();
      
  switch (result) {
    case 1:
      Serial.println("well-lighted");
      break;
    case 2:
      Serial.println("get dark");
      break;         
  }
  lastResult = result;
  cntChange++;
    
  if (sleepMinOnChange > 0 && cntChange > 1) {
    ESP.deepSleep(sleepMinOnChange * 60 * 1000* 1000);
  }
}
```

# その後

見た目があやしすぎる、とお叱りの声を多数いただきまして、100円ショップで適当なケースを探してきました。
センサーを覆っていますが、明るさの違いはなんとか検出できています。

![DSC_0024.JPG](https://qiita-image-store.s3.amazonaws.com/0/131050/ba5cba5e-2327-31a6-8fb8-2f7ae47bf29a.jpeg)

# おまけ

とある人にHTTPで状態取得できるようにしてほしいと言われ、Node.jsでHTTPサーバを立てました。

```js:httpd.js
var resultStatus = '0';

const http = require('http');
const server = http.createServer((req, res) => {
  function respond(code, body) {
    switch (code) {
      case 404:
        body = '404 Not Found';
        break;
      case 405:
        body = '405 Method Not Allowd';
        break;
      case 500:
        body = '500 Internal Server Error';
        break;
    }
    res.writeHead(code, {'Content-Type': 'text/plain'});
    res.end(body);
  }

  if (req.method != 'GET') {
    respond(405);
    return;
  }

  var params = req.url.split('/');
  if (params[1] != 'light') {
    respond(404);
    return;
  }
  switch (params[2]) {
    case 'DEVICE_ID':
      break;
    default:
      respond(404);
      return;
  }

  switch (resultStatus) {
    case '1':
      respond(200, 'true');
      break;
    case '2':
      respond(200, 'false');
      break;
    default:
      respond(500);
  }
});
server.on('clientError', (err, socket) => {
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(80);

const mqtt = require('mqtt');
const client = mqtt.connect('mqtt://localhost:1883', {'username': 'USERNAME', 'password': 'PASSWORD'});
client.on('connect', () => {
  client.subscribe('DEVICE_ID/result');
});
client.on('message', (topic, message) => {
  var params = message.toString().split(' ');
  resultStatus = params[1];
});
```