社内のトイレ難民解決!トイレ空き状況確認システムを作ってみた

TOWN株式会社の今のオフィスは個室トイレが1つしかない。

トイレに行って空いていなくて待っているか、席に戻って開くのを待っている。

トイレに行く際もいちいちセキュリティドアを出ないといけないので、戻るのもめんどくさい。

トイレ難民続出中。。。

なので、その場で個室の空き状況が確認できたら便利だと思って空き状況確認システムを作ってみた!

構成図

最初はAPI GatewayとLambdaは使わずに、Google Apps ScriptとGoogle Spread Sheetで作ろうと思って実装してたけど、センサーがGoogleのリダイレクトに対応できなくて諦めました。笑
(リダイレクトに対応したHTTPSRedirectというセンサーのモジュールがあるのですが、上手く動作しなかった。)

事前に用意するもの

ESPr® Door Sensor + FTDI USBシリアル変換アダプター

画像引用: ESP-DOOR – スイッチサイエンス

センサー周りの操作

センサーにプログラムを書き込むにはArduino IDEが必要です。

こちらからダウンロードしました。

環境設定

環境設定を開いて以下のように設定します。

追加するURLはこちら https://arduino.esp8266.com/stable/package_esp8266com_index.json

次に ツール > ボード > ボードマネージャー **をクリックしてESP8266**をインストール。

最後に** ツール > ボード から「Generic ESP8266 Module」**を選択して以下のように設定で終了。

スケッチのマイコンボードに書き込むからプログラムを書き込めます。

※シリアルケーブルの基盤にある3pinの部分をPROG2本に挿すと書き込みモード、PROGとRUNに挿すと実行モードになります。

コード

事前に作っておいてAPI GatewayのWebhookのドメイン部分、それ以外の部分を以下のコードに入れます。

今回API Gatewayに送信するPOSTデータ

{
    "gender": "性別(今回はMANのみの実装)",
    "value": "開閉値"
}

センサーに書き込むコード

#include <ESP8266WiFi.h>
#include <HTTPSRedirect.h>
#include <DebugMacros.h>

#define HTTPS_PORT 443
#define HOST "Amazon API GatewayのWebhookのドメイン部分"
#define URL "Amazon API GatewayのWebhookのドメイン以降部分"
#define CLOSE 0
const char* ssid     = "WiFi SSID";
const char* password = "WiFi Pass";

HTTPSRedirect* client = nullptr;

extern "C" {
  #include "user_interface.h"
}

boolean flag = false;
int LED = 4;
int reed_sw = 5;

void setup() {
  Serial.begin(74800);
  delay(10);

  // We start by connecting to a WiFi network
  Serial.println();
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

}

void loop() {
  int door_state;
  door_state = digitalRead(reed_sw);

  Serial.print("Door State:");
  if(door_state == CLOSE){
    Serial.println("Close");
  }
  else{
    Serial.println("Open");
  }

  // Use WiFiClient class to create TCP connections
  Serial.println(doRedirectGet(door_state));

  delay(10);

  if(door_state == CLOSE){
    Serial.println("DEEP SLEEP 60s");
    ESP.deepSleep(0, WAKE_RF_DEFAULT); //ドアが閉じている間はドアが開くまで待機
  }
  else{
    Serial.println("DEEP SLEEP");
    ESP.deepSleep(0, WAKE_RF_DEFAULT); //ドアが開いている間はドアが閉じるまで待機
  }
  delay(1000);
}

// Redirectを回避してHTTP-GETする
String doRedirectGet(int door_state) {
  String body = "";
  client = new HTTPSRedirect(HTTPS_PORT);
  client->setPrintResponseBody(false);
  client->setContentTypeHeader("application/json");

  Serial.print("Connecting to ");  Serial.println(HOST);
  // 10回まで接続を行う
  bool connect_flag = false;
  for (int i=0; i<10; i++) {
    int retval = client->connect(HOST, HTTPS_PORT);
    if (retval == 1) {
      Serial.print("Connected to ");   Serial.println(HOST);
      connect_flag = true;
      break;
    }
    else {
      Serial.println("Connection failed. Retrying...");
    }
  }
  if (!connect_flag) {
    Serial.print("Connection failed to server: ");   Serial.println(HOST);
    return "";
  }

  // payload判定
  String state = "";
  if(door_state == CLOSE){
    state = "CLOSE";
  } else {
    state = "OPEN";
  }
  String payload = "{\"gender\": \"MAN\", \"value\":\"" + state + "\"}";

  // POST
  bool post_flag = false;
  for (int i=0; i<10; i++){
    client->POST(URL, HOST, payload);
    body = client->getResponseBody();
    if (client->getStatusCode() == 200) {
      Serial.println("POST Success");
      post_flag = true;
      break;
    }
  }
  if (!post_flag) { 
    Serial.println("POST Failed");
  }
  Serial.println("closing connection");
  client = nullptr;
  delete client;
  return body;
}

Lambdaでの操作

今回はS3をデータベースとして使いました。

なので、ロールにはS3FullAccessをアタッチしました。

また、S3には以下のようなJSONを置いておきました。

{
    "MAN": "OPEN",
    "WOMAN": "CLOSE"
}

コード

トイレに入るたびにSlackに通知が来ては邪魔なので、Slackのメッセージ更新機能を使いました。

細かいメソッドについてはこちら

トークンを使ってLambdaやCurlコマンドからあらかじめメッセージを送って置いて、それを更新する感じです。

更新にはts(タイムスタンプ)が必要なので、SlackのWeb版でコメントのリンクをコピーをすると「1543312999067300」みたいなのが取れるので、「1543312999.067300」のように右から6桁目に小数点を入れます。

Channel IdもWeb版のURL(https://.slack.com/messages/〇〇〇〇/)に表示されます。

また、Lambdaにはない外部ライブラリを使うので、ローカルでpip install slackclient -t ./ などとやってディレクトリにライブラリをインストールし、Lambdaのスクリプトも含めてzipで固めてアップロードしました。(やり方はググれば出てきます)

import json
import boto3
from datetime import datetime
from slackclient import SlackClient

# S3(DB)
S3_BUCKET_NAME = ''
S3_DB_NAME = ''
S3_client = boto3.client('s3')
response = S3_client.get_object(Bucket=S3_BUCKET_NAME, Key=S3_DB_NAME)
DB = json.loads(response["Body"].read())

# Slack
slack_token = ''
slack_client = SlackClient(slack_token)
channel_id=""
ts_num="" #残すメッセージts


def channel_list(slack_client):#使わない
    channels = slack_client.api_call("channels.list")
    if channels['ok']:
        return channels['channels']
    else:
        return None

def send_message(slack_client, channel_id, message): #使わない
    response = slack_client.api_call(
        "chat.postMessage",
        channel=channel_id,
        text=message,
    )
    return response

def update_message(slack_client, channel_id, message,ts):
    response = slack_client.api_call(
        "chat.update",
        channel=channel_id,
        text=message,
        ts=ts
    )
    return response

def delete_message(slack_client,channel_id,ts):
    response = slack_client.api_call(
        "chat.delete",
        channel=channel_id,
        ts=ts
    )
    return response

def get_history(slack_client,channel_id): # 使わない
    response = slack_client.api_call(
        "channels.history",
        channel=channel_id,
    )
    return response

def print_json(json_data):
    return print("{}".format(json.dumps(json_data,indent=4)))



def lambda_handler(event, context):
    date = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
    gender = event["gender"]
    value = event["value"]

    # DB更新
    DB[gender] = value
    S3_client.put_object(Body=json.dumps(DB, indent=4), Bucket=S3_BUCKET_NAME, Key=S3_DB_NAME)

    # Slack output    
    Man_data = value
    output = "\tMan 👨       :`" + Man_data + "`\n" + "Woman 👩 :`Coming soon...`"

    for message in get_history(slack_client,channel_id)["messages"]:
        ts=message["ts"]
        if ts != ts_num:
            response=delete_message(slack_client,channel_id,str(ts))

    response=update_message(slack_client, channel_id, output, ts_num)

    return {
        'statusCode': 200,
        'body': date + " " + gender + " " + value 
    }

動作確認

設置するとこんな感じ。上に丁度いい凹みがあったので、そこに電池類は配置、センサーだけにゅっと出してあります。

上から見るとこう。この凹みがあるおかげでバッテリーもそこまで目立ちません。

トイレ内部から見るとこんな感じでかなりきれいに隠れてます。

なので、トイレの中に入っている人が不審な機器がある!と怖がらずに済みます。

内側の方が簡単だけど、なんか機器あったら怖い。。。

動作させた動画がこちら。

女子トイレはまだ実装していないのでComing Soonです。

表示はこんな感じ。

これでトイレ難民を救えた!

かかる費用はこんな感じ。

「トイレの空き状況確認システム」のお値段

2018/12/25追記

2000mAhのモバイルバッテリーで動作をさせていましたが、11月28日に動作開始で約1ヶ月程度電池はもちました。

序盤は開け締めやテストを行っていたのを、そのまま使用したので1ヶ月程度は2000mAhのバッテリーでも持ちそうです。

開発メンバー