LoginSignup
29
18

More than 3 years have passed since last update.

おうちハック 〜サイコロキャラメルに収まる、可愛くて、実用的な食洗機回し忘れ通知システムを作る〜

Posted at

はじめに

サイコロキャラメルに収まる無線センサー(下図の右上)を使って、見た目が可愛く、かつ実用的な、おうちハックシステムを作っていきます。

今回は(妻の要望により)食洗機の回し忘れを検知してSlackに通知するシステムというニッチなお題で作っていきますが、このシステムアーキテクチャ自体は他にも転用がきくものになっていますので、おうちハックの参照アーキテクチャとしてお読みいただければと思います。

なお、実用路線では、他にもLEGOでスマートロックを作ったりしています(稼働実績1年半)。こちらもアイディアのご参考にどうぞ。

下図が今回開発する通知システムです。
システム概要.jpg

免責事項

著者は本記事を掲載するにあたって、その内容、機能等について細心の注意を払っておりますが、内容が正確であるかどうか、安全なものであるか等について保証をするものではなく、何らの責任を負うものではありません。
本記事内容のご利用により、万一、ご利用者様に何らかの不都合や損害が発生したとしても、著者や著者の所属組織は何らの責任を負うものではありません。

なぜ食洗機回し忘れ?/システム機能

同じ悩みを抱えている家庭が世界にどのくらいあるのか怪しいものですが、、、
子供の面倒でバタバタしていたりして、食洗機に皿を入れたはいいが、つい食洗機を動かし忘れて寝てしまい、
朝になって「洗ってないじゃん!」というガッカリが1ヶ月に1回くらい起きていました。

そのガッカリを減らすことを目標に、次のシンプルな機能をもつシステムを作りました。

  • 機能: 晩ご飯(19:00)以降に食洗機が回っていなかったら、21:55になったらSlackへリマインド通知を飛ばす。
  • UX: 旅行などで、家に誰もいない場合は通知を飛ばさない。
  • 運用性: 数年間(10年近く)電池を交換しなくても稼働し続ける。

作り方を順番に説明して行きます。

システム構成

無線センサーTWE-Lite 2525AやRaspberry Pi、iOS(HomeKit)などを活用して、このシステムを作ります。

今回利用する無線センサーTWE-Lite 2525A(正確には、センサーを無線化する通信装置)は、下図のように、ちょうどサイコロキャラメルに収まる寸法になっています(見えませんが中にピッタリ収まっています)。
センサーは目につくところに置くケースが多いと思いますので、見栄えは大事ですね。可愛いと愛着が湧きます。
サイコロキャラメルに収まる無線センサー.jpeg

システム構成のポイント

システム構成は下図のようになります。TWE-Lite以外は比較的メジャーな技術を使って作ります。
システム概要.jpg

このシステムの仕組みのポイントは次のとおりです。

  1. TWE-Lite 2525Aに食洗機の温度を計測する温度センサーSHT21を接続して無線化します。温度センサーは食洗機の表面に貼り付けます。
  2. Raspberry PiにUSB接続されたMoNoStickTWE-Lite 2525Aと無線通信(IEEE802.15.4)します。
  3. 食洗機が稼働していると表面が少し暖かくなります。5分ごとに温度を測定し、温度がある閾値(32度)を超えているとき、食洗機は稼働中であるとみなします。
  4. Raspberry Piに食洗機の稼働状態を取得したり、稼働していなければSlackにリマインド通知するWeb APIを立てます。
  5. 自製のデバイスをHomeKitで扱えるようにするhomebridgeのサーバをRaspberry Piに立て、HomeKitに食洗機の状態確認と稼働チェックを行うボタン(スイッチデバイス)を追加します(裏側で上記のWeb APIを呼び出します)。
  6. 誰かが自宅にいることの確認はHomeKitの機能(iPhoneが家のネットワークに接続したら在宅とみなす)を使って実現します。
  7. HomeKitのオートメーションを使い、誰かが家にいるときにのみ、毎日21:55になったら食洗機の稼働チェック(稼働していなければSlackに通知)を行います。
  8. 今回不要な加速度センサーをTWE-Lite 2525Aから外し、温度の測定間隔を5分にすることでバッテリーを10年近く持つようにします。

システム開発

この食洗機回し忘れ通知システムの開発を、ハードウェア/ソフトウェア/設定の3つに分けて解説します。

前提条件

時間の都合で、検索すれば比較的容易に情報を入手できる下記事項に関する説明は省略します。

  • iOSのHomeKitの知識
  • Raspberry Piのセットアップ
  • Raspberry PiへのPython 3やflask等Pythonライブラリのインストール
  • Raspberry Piでのhomebridge(HomeKitデバイスを自作するためのソフトウェア)のインストール、設定方法、iOSとの連携設定等(参考: homebridgeのインストール方法
  • Slackのボット作成(APIトークンの取得、特定チャネルへのメッセージ送信)

開発に必要なもの

  1. Slackアカウント: リマインドの通知先
  2. HomeKitが使えるiOSデバイス(iPadやApple TV): HomeKitのハブ
  3. Raspberry Pi: MoNoStickのUSBホスト、Web APIサーバ、homebridgeサーバ
  4. TWE-Lite 2525A: 温度センサーSHT21を接続する無線通信の子機
  5. MoNoStick: TWE-Lite 2525Aと無線通信する親機(USBデバイス)
  6. TWE-LITE-R: TWE-Lite 2525Aにプログラムを書き込むデバイス
  7. サイコロキャラメルの箱: TWE-Lite 2525Aを格納する箱
  8. 東急ハンズの「プラスチックキューブ 角 クリア 30×30×30mm」: TWE-Lite 2525Aを格納したサイコロキャラメルを格納する箱
  9. 電池CR3032: TWE-Lite 2525Aの電池
  10. SHT21: 温度センサー
  11. 4.7kΩ抵抗 x 2や電子回路基板や線など、SHT21センサーをTWE-Lite 2525Aに繋ぐための電子部品
  12. 東急ハンズのプラスチックキューブに穴を開けるドリルやカッター(怪我には十分注意してください)

ハードウェア開発

無線食洗機状態センサー(無線温度センサー)の作り方を説明します。

  1. 加速度センサーを外します。
    • 10年くらいバッテリーを持たせたい場合は、TWE-Lite 2525Aの基板から加速度センサーを外します。外さない場合は、バッテリーは9ヶ月前後でなくなります。一度外すと再度付けることは困難であり、加速度センサーは二度と使わない覚悟がいります。
    • 加速度センサーは下図の場所にあるチップです。外すのにはコツがいります。電子工作に慣れている人にお願いするといいでしょう。
      TWE-Lite2525.A 加速度センサー.png
  2. 電子回路を作ります。
    • 無線温度センサーは、無線通信デバイスTWE-Lite 2525Aに温度センサーSHT21を接続して作ります。
      接続するための電子回路は https://mono-wireless.com/jp/products/TWE-APPS/App_Tag/mode_SHT21.html の「子機の回路図の例」です(SDA/SCLに4.7kΩのプルアップ抵抗を接続すればOK)。
    • 温度センサーを除いた電子回路基板とTWE-Lite 2525Aはサイコロキャラメルに収めますので、できる限り小さく作る必要があります。下図を参考にしてください(右上の部分がサイコロキャラメルやプラスチックキューブから出るようにケースを加工することになります)。
      電子回路実装例.jpg
  3. ケースに収めます。
  4. 温度センサーSHT21を動かすためのファームウェアをTWE-Lite 2525A(無線子機)とMoNoStick(無線親機)に書き込みます。
    • ファームウェアは https://mono-wireless.com/jp/products/TWE-APPS/App_Tag/download.html の「親機・子機・中継機用」からzip圧縮されたものをダウンロードできます。zipファイルを解凍してできる次のファイルがそれぞれのファームウェアです。
      • TWE-Lite 2525A用: App_Tag-EndDevice-BLUE-LITE2525A.bin
      • MonoStick用: App_Tag-Parent-BLUE-MONOSTICK.bin
    • TWE-Lite 2525Aへのファームウェア書き込みにはTWE-LITE-Rを使用します。TWE-LITE-RとTWE-Lite 2525Aの結線方法とファームウェア書き込み手順は次のページに書かれています。Raspberry PiにTWE-LITE-Rを接続し、linuxの手順に従い書き込むことをおすすめします。
      • 結線: https://mono-wireless.com/jp/products/TWE-Lite-2525A/firmware_update.html
      • ファームウェア書き込み手順: https://sdk.twelite.info/twelite-sdkno/fumuua/tweterm.py
      • 設定: ファームウェア書き込み手順に従い書き込み後、インタラクティブモードに入り、次のように設定します。
        
        a: set Application ID (変更不要です) 
        i: set Device ID (1) ← センサーが複数ある場合に見分けるIDになります。今回は1にしてください。
        c: set Channels (変更不要です)
        x: set Tx Power (変更不要です)
        b: set UART baud (変更不要です)
        B: set UART option (変更不要です)
        k: set Enc Key (0x........) ← 通信暗号化のキーです。推測しにくい値を設定してください。
        o: set Option Bits (0x00001400) ← 通信暗号化 + OTA禁止
        d: set Sleep Dur (300000) msec ← 5分に一回、温度測定結果を無線送信します。
        w: set Sensor Wait Dur (30)
        m: set Sensor Mode (0x31) ← SHT21を用いるモード
        p: set Sensor Parameter ( )
        P: set Sensor Parameter2 ( )
        
        設定値の意味: https://mono-wireless.com/jp/products/TWE-APPS/App_Tag/interactive.html
    • MoNoStickへの書き込み方法も同様です。Raspberry PiにMoNoStickを接続し、linuxの手順に従い書き込むことをおすすめします。
      • ファームウェア書き込み手順: https://sdk.twelite.info/twelite-sdkno/fumuua/tweterm.py
      • 設定: ファームウェア書き込み手順に従い書き込み後、インタラクティブモードに入り、次のように設定します。
        
        a: set Application ID (変更不要です)
        c: set Channels (変更不要です)
        x: set Tx Power (変更不要です)
        b: set UART baud (変更不要です)
        B: set UART option (変更不要です)
        k: set Enc Key (0x........) ← 通信暗号化のキーです。TWE-Lite 2525Aの設定値と同一にします。
        o: set Option Bits (0x00001000) ← 通信暗号化 + 標準形式出力
        
        設定値の意味: https://mono-wireless.com/jp/products/TWE-APPS/App_Tag/interactive.html

ソフトウェア開発

Web APIサーバを作ります。作るAPIは次の2つです。

  1. 食洗機の稼働状態を返すAPI: 下記コードの GET '/api/' + API_SECURE_KEY + '/dishwasher'
  2. 食洗機を回し忘れている場合にリマインド通知を飛ばすAPI: 下記コードの POST '/api/' + API_SECURE_KEY + '/dishwasher/remind'

プログラムはシンプルです。
Raspberry PiにUSB接続されたMoNoStickとシリアル通信して、食洗機に貼り付けられた温度センサーの値を取得します。
温度が32度を超えたら食洗機が稼働しているとみまします。
リマインド通知を飛ばすAPIが呼び出されたとき、19時以降に食洗機が稼働していなければSlackにリマインドを通知します。

以下、Web APIのプログラムです。
Web APIはFlaskを用いて実装しています。

token, home_channel, botname, API_SECURE_KEY, API_PORTにはそれぞれ適切な値を指定してください。
指摘すべき値はコード中の説明を参考にしてください(わざと構文エラーが出るようにしています)。

dishwasher.py
import serial
import datetime
import sys
import threading
import time
import urllib
import urllib.request
import urllib.parse
import json
import logging
import threading
from flask import Flask, jsonify, abort, make_response

logging.basicConfig(level=logging.WARNING, format='%(asctime)s:%(levelname)s:%(name)s: %(message)s')
logger = logging.getLogger(__name__)
logger.setLevel(10)

argv = sys.argv
argc = len(argv)

# slackの設定
slack_api_url = "https://slack.com/api/chat.postMessage"
token = slackボットのトークンをここに指定してください
home_channel = リマインダの追地先となるチャネル名をここに指定してください
botname = slackに通知するときのボット名をここに指定してください

API_SECURE_KEY = 少しセキュアにするためランダムな推測しにくい文字列をここに指定してください
API_PORT = APIを動かすポート番号を指定してください

# 食洗機が稼働しているときの温度
TEMP_THRESHOLD = 32.0
# 食洗機が動いていてほしい時刻の開始時間
NIGHT_HOUR = 19

# バッテリー不足警告の閾値
THRESHOLD_LOW_BATT_KEY = 2100.0

JST = datetime.timezone(datetime.timedelta(hours=+9), 'JST')


def send_message(channel, message):
    """slackにメッセージを送る"""
    try:
        req_values = {
            "channel": channel,
            "username": botname,
            "text": message
        }
        headers = {
            "Content-Type": "application/json; charset=utf-8",
            "User-agent": "Mozilla/5.0 (Linux i686)",
            "Authorization": "Bearer {}".format(token)
        }
        json_data = json.dumps(req_values).encode("utf-8")
        req = urllib.request.Request(slack_api_url, data=json_data, headers=headers, method="POST")
        response = urllib.request.urlopen(req)
        response_body = response.read()
    except Exception as e:
        # print(e)
        pass


# 食洗機が動いた最後の日時
washing_status_lock = threading.Lock()
last_washing_datetime = datetime.datetime.now(JST) - datetime.timedelta(days=1)
is_washing = False


def set_washing_status(washing_datetime, washing):
    global last_washing_datetime, is_washing
    try:
        washing_status_lock.acquire()
        last_washing_datetime = washing_datetime
        is_washing = washing
    finally:
        washing_status_lock.release()


def get_washing_status():
    global last_washing_datetime, is_washing
    try:
        washing_status_lock.acquire()
        return last_washing_datetime, is_washing
    finally:
        washing_status_lock.release()


# 食洗機センサーのバッテリー
# dishwasher_sensor_batt = -1
temp_batt = -1

# エラー発生
error_occurred = False


def m_thread(ser):
    """UARTの出力を読み込んで様々な処理を行う(実質メイン関数)"""
    global temp_batt, error_occurred
    # global dishwasher_sensor_batt

    now = datetime.datetime.now(JST)

    while True:
        try:
            # UARTを読み込んでリスト化
            line = ser.readline().decode("utf-8").strip()
            sline = line.split(":")
            now = datetime.datetime.now(JST)

            if len(sline) == 12 and sline[10].startswith("te=") and sline[11].startswith("hu="):
                # 温湿度計
                # ::rc=80000000:lq=159:ct=000E:ed=81020ACC:id=1:ba=2900:a1=1046:a2=0549:te=2740:hu=4609
                # print("{}; {}".format(now, line))
                cid = sline[6][3:]
                te = float(sline[10][3:]) / 100
                hu = float(sline[11][3:]) / 100
                prev_batt = temp_batt
                temp_batt = float(sline[7][3:])
                # print "te=" + str(te) + ", hu=" + str(hu)

                if temp_batt < THRESHOLD_LOW_BATT_KEY <= prev_batt:
                    send_message(home_channel, "食洗機モニタの電池が切れそうだよ。")

                if cid == "1" and te >= TEMP_THRESHOLD:
                    last_datetime, is_washing = get_washing_status()
                    set_washing_status(now, True)
                else:
                    last_datetime, is_washing = get_washing_status()
                    set_washing_status(last_datetime, False)

        except Exception as e:
            if not error_occurred:
                error_occurred = True
                error_msg = "ERROR {0} {1}".format(str(now), str(e))
                print(error_msg)


api = Flask(__name__)


@api.errorhandler(500)
def error_handler(error):
    """エラーメッセージを生成するハンドラ"""
    response = jsonify({'cause': error.description['cause']})
    return response, error.code


@api.route('/api/' + API_SECURE_KEY + '/dishwasher', methods=['GET'])
def get_dishwasher_status():
    """食洗機の稼働状態を取得するAPI"""
    last_datetime, washing = get_washing_status()

    result = {
        "washing": washing,
        "last_washing_datetime": last_datetime.strftime("%Y-%m-%d %H:%M:%S%z")
    }
    return make_response(jsonify(result))


@api.route('/api/' + API_SECURE_KEY + '/dishwasher/remind', methods=['POST'])
def remind_dishwasher_status():
    """食洗機が所定の時間以降に動いていなかった場合、リマインダーをslackに送るAPI"""
    last_datetime, washing = get_washing_status()
    now = datetime.datetime.now(JST)
    today_night_datetime = now - datetime.timedelta(hours=now.hour - NIGHT_HOUR) - datetime.timedelta(minutes=now.minute) - datetime.timedelta(seconds=now.second) - datetime.timedelta(microseconds=now.microsecond)
    washed = now.hour < NIGHT_HOUR and last_datetime >= (today_night_datetime - datetime.timedelta(days=1)) or now.hour >= NIGHT_HOUR and last_datetime >= today_night_datetime
    if not washed:
        send_message(home_channel, "食洗機を動かし忘れているかも。{}時以降に食洗機が動いてないよ。".format(NIGHT_HOUR))

    result = {
        "washed": washed
    }
    return make_response(jsonify(result))


if __name__ == "__main__":
    # 引数がある場合、引数で指定したデバイスを用いる
    if argc != 2:
        ser = serial.Serial("/dev/ttyUSB0", 115200, timeout=1) # MoNoStickはだいたい/dev/ttyUSB0にある
    else:
        ser = serial.Serial(argv[1], 115200, timeout=1)

    # 別スレッドで実行
    t = threading.Thread(target=m_thread, args=(ser,))
    t.setDaemon(True)
    t.start()

    try:
        api.run(host='0.0.0.0', port=API_PORT)
    finally:
        ser.close()

homebridgeとHomeKitの設定

Web APIをiOSデバイスから呼び出せるようにするためにhomebridgeを利用します。
homebridgeのhomebridge-cmdaccessoryプラグインをインストールし、下記のhomebridge設定(config.json)を用いてhomebridgeサーバ(Raspberry Piに立てる想定)を起動するとHomeKitに2つのボタン「食洗機」「食洗機リマインド」が追加されます。
それぞれ先ほど作成したWeb APIに対応します。

なお、設定ファイル中の{APIサーバ}はWeb APIが動作しているサーバのIP、{API_PORT}と{API_SECURE_KEY}はそれぞれWeb APIのソースコード中にあった同名の環境変数の値に置き換えてください。

config.json
{
    ...省略...

    "platforms": [{
        "platform": "cmdAccessory",
        "name": "CMD Switch",
        "switches": [{
            "name": "食洗機",
            "on_cmd": "echo 1",
            "off_cmd": "echo 0",
            "state_cmd": "curl -sS -X GET http://{APIサーバ}:{API_PORT}/api/{API_SECURE_KEY}/dishwasher | grep 'true'",
            "polling": true,
            "interval": 3,
            "manufacturer": "Isao Sonobe",
            "type" : "Switch"
        }, {
            "name": "食洗機リマインド",
            "on_cmd": "curl -sS -X POST http://{APIサーバ}:{API_PORT}/api/{API_SECURE_KEY}/dishwasher/remind",
            "off_cmd": "echo 0",
            "state_cmd": "echo '1' | grep '0'",
            "polling": true,
            "interval": 3,
            "manufacturer": "Isao Sonobe",
            "type" : "Switch"
        }]
    }]
}

下図のようなボタンがHomeKitに登録されたら成功です。
HomeKit.png

最後に、誰かが家にいるときのみ、21:55になったときに回し忘れチェックを実行するよう、HomeKitオートメーションを設定します。
オートメーションの設定内容は下図を参考にしてください。「いつ」を「誰かが家にいるときのみ」の21:55にするのがポイントです。

HomeKit Automation.png

長かったですが、これでシステムは完成です!
無線温度センサー/Web API/HomeKitが連携して動き始めます。

オチとその先へ

長い記事をお読みいただき、ありがとうございました。

実は、この話には悲しいオチがあります。。。
「Qiita Advent Calendarで何を書こうねぇ」と同僚と話をして、いくつかあった候補の中からこのおうちハックを選び、記事にするぜと心に決めて帰宅してみたら、なんと妻に破壊されていました・・・。
サイコロがー.png
サイコロがーーー!!跡形もない!
妻の話では、お尻がぶつかって、ケースを破壊してしまったそうです。ガビーン
怪我はなかったのが幸いではあります・・・。

そんなこんなで、弔いのために記事にして、次は小さく改良(するのは簡単なので、かつ、可愛さありに)することを固く心に決めた夜でした。

今回、開発した食洗機回し忘れ通知システムはかなりニッチでした。
次は、もう少し欲しい人が多そうな床暖房消し忘れセンサーと屋外からオン/オフできる仕組みを作りたいと思っております。
冬が終わる前には。

29
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
18