ドアの開閉をWEBカメラで通知したい

RaspberryPi

その昔、玄関に鍵がかかっていることが珍しかった昭和の時代。呼び鈴ボタンがあるにも関わらず、玄関ドアをいきなりガラッと開けて「ごめんください」と叫ぶのが普通だった時代。しかし当時存命だった祖母は耳が遠くなってきていて誰が来たかわからない。そこでオヤジは玄関チャイムを自動的に鳴らすようにマイクロスイッチを玄関ドアに付けて、呼び鈴ボタンのON/OFFが玄関ドアの開閉に連動するようにして、強制的に呼び鈴チャイムが鳴るようにしたのであった。これ、昭和50年代の話であるが、実家においては今なおそれが稼働中である。

その現代版、というわけではないが、磁気センサースイッチ(マグネットスイッチ)によってドアオープンの検出をして、ドアが開いたら通知をしようというわけ。

きっかけは子が中学生になる年の冬、児童クラブからもいずれは「卒業」して鍵っ子となるが、いちいち帰って来たかどうか確認するのは面倒くさい、子としても「鬱陶しい」と感じるようになるだろうから先手管理でやってみようと。もちろん泥棒さんが侵入してもわかるはずで(笑)

まず入手すべきはマグネットスイッチ。中に磁石に反応する金属が仕込んであるスイッチ部分と磁石そのもの、2つに分かれたスイッチである。お互いを近づけるとON、離すとOFF(またはその逆のものもある)になる。これをドアフレームとドア扉に付けておくと、ドアが閉まったらON、ドアが開いたらOFFになるわけですな。通常時クローズをノーマルクローズ(NC)、その逆はノーマルオープン(NO)という。どちらでも構わない(設定で反対にすればいい)が、ドアクローズではスイッチON(スイッチ閉)状態、ドアオープンではスイッチOFF(スイッチ開)がなんとなくイメージしやすい。

この状態をラズパイのGPIOに監視させるだけ。スイッチがONなら何もしない、スイッチがOFFになる状態を検出するようなエッジ検出するプログラムを作り、スイッチがOFFになったら「何かを実行する」という状態のデバッグを行う。何故かいまだにGPIOをコントロールするにはRPi.GPIOでの作例が幅を利かせているが、RPi.GPIOで出来ることは全て可能とされるgpiozeroにて書いてみる。

from gpiozero import Button
from time import sleep
import subprocess

# 初期化
SW_GPIO = 21

# ButtonクラスでGPIOピンを指定し、プルアップを指定
button = Button(SW_GPIO, pull_up=True)

# 状態の初期化
old_state = button.is_pressed

# 無限ループ
while True:
    try:
        # 現在の状態を取得(False: ボタンが押されている, True: ボタンが離されている)
        new_state = button.is_pressed

        # 条件分岐
        if old_state and not new_state:
            print('Open!')
        else:
            print('Close')
        sleep(1)
    except:
        # エラーや強制終了の場合
        print('Err')
        break

# 終了処理
print('End')

これをsw.pyなどと命名して保存。

GPIOの21とGNDをショート、つまりスイッチをONした状態を擬似的に作り、実行。

Close・・・と出力される。つまり、ドアが閉じている、スイッチが押されている、という状態を示している。ここで、線を片方抜いてみる。

スイッチをOFFした状態にする。

CloseがOpen!に変わる。そのまま線を再接続すると、

再びCloseに変わる。はい、ドアの開閉検出はできた。

これと平行して、USBカメラを用意する。ラズパイ公式のカメラモジュールより設置の点では自由度が高いし安いのでUSBカメラの方が良い。ロジクールのC270が安価で安定しているということで、これを使うことにした。某フリマアプリで800円で入手。UVC(USB Video Class)に対応したカメラなら、ほとんど何もしなくても(ドライバ不要、OSを問わない)動作するので、普段Macを使って子はWindowsを使い、たまにこうしてラズパイ使ったりするような人はそういうデバイスを買うと良い。

pi@pi2:~ $ lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

この状態から、USBカメラを接続すると、

pi@pi2:~ $ lsusb
Bus 001 Device 004: ID 046d:0825 Logitech, Inc. Webcam C270
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

はい、認識。で、fswebcamをインストール。

sudo apt-get install fswebcam

これで、撮影できるようになる。

pi@pi2:~ $ fswebcam image.jpg
--- Opening /dev/video0...
Trying source module v4l2...
/dev/video0 opened.
No input was specified, using the first.
Adjusting resolution from 384x288 to 352x288.
--- Capturing frame...
Captured frame in 0.00 seconds.
--- Processing captured image...
Writing JPEG image to 'image.jpg'.

これでimage.jpgに撮影した画像が保存される。保存された画像はこれ。

全くパラメーターを与えていないので、画質は良くない。ここから色々調整する必要があるらしい。それは当然カメラによっても違う。

調整するとこのくらいにはなるが、まぁこんなもんか?画質や解像度の大きさはファイルの大きさになるので、そこはうまいこと調整する。LINEでは「玄関に誰が入ってきたか」がわかればいいので画質はそんなに重要ではない。

次に、最初に作った「ドアの開閉検出」のソフトとこのfswebcamを組み合わせて「スイッチがOFFにになったら(=ドアが開いたら)撮影する」というソフトを作り上げて、さらにLINE Notifyに連動させる(当初はSlackを使用していたがそれにしか使用しないので辞めた)LINE Notifyがサービス終了したので再びSlackに戻して連動させた。

LINE Notifyに関してはこっちに記載。

これで「玄関ドアが開いたらその時の玄関を撮影してLINEに画像を送信する」というドアの開閉をUSBカメラで撮影した画像と共に通知するシステムが出来上がる。

  • ドアが閉まっている=Close
  • ドアが開く=Close->Open
  • 少ししてから撮影、LINESlack送信
  • ドアが閉まる=Open->Close

この、Close->Openの状態変異一回だけを検知しなければならない。そうしないとドアがOpenの間、撮影とLINE送信Slack送信を繰り返してしまう。その辺を加味して作ったプログラムがこれ。

まず、LINE部分は分けておく(line.py)slack_notify.pyは分けておく。

import requests
import os
import json
import traceback

SLACK_TOKEN = "xoxb-…"        # Bot の OAuth Token
SLACK_CHANNEL = "C0123456"    # 投稿先チャンネル ID

def slack_send_file_external(message: str, file_path: str):
    try:
        filename = os.path.basename(file_path)
        file_size = os.path.getsize(file_path)

        # ---- ステップ 1: upload URL を取得 ----
        url_get = "https://slack.com/api/files.getUploadURLExternal"
        headers = {"Authorization": f"Bearer {SLACK_TOKEN}"}
        data = {"filename": filename, "length": file_size}
        resp = requests.post(url_get, headers=headers, data=data)
        respj = resp.json()
        if not respj.get("ok"):
            print("getUploadURLExternal error:", respj)
            return

        upload_url = respj.get("upload_url")
        file_id = respj.get("file_id")

        # ---- ステップ 2: upload_url にファイルをPUT ----
        with open(file_path, "rb") as fp:
            upload_resp = requests.put(
                upload_url,
                data=fp,
                headers={"Content-Type": "application/octet-stream"}
            )
        if upload_resp.status_code != 200:
            print("upload failed:", upload_resp.status_code, upload_resp.text)
            return

        # ---- ステップ 3: completeUploadExternal ----
        url_complete = "https://slack.com/api/files.completeUploadExternal"
        headers2 = {
            "Authorization": f"Bearer {SLACK_TOKEN}",
            "Content-Type": "application/json; charset=utf-8",
        }
        payload = {
            "files": [{"id": file_id, "title": filename}],
            "channel_id": [SLACK_CHANNEL],
            "initial_comment": message
        }
        complete_resp = requests.post(url_complete, headers=headers2, json=payload)
        complete_j = complete_resp.json()
        if not complete_j.get("ok"):
            print("completeUploadExternal error:", complete_j)
            return

        print("File sent OK:", complete_j)

    except Exception as e:
        print("Slack送信エラー:", e)
        traceback.print_exc()


if __name__ == '__main__':
    file_path = "/home/pi/image.jpg"
    slack_send_file_external("ラズパイから写真を送信", file_path)

そして、こっちがメイン(line_door_zero.pyslack_door_zero.py)。

from gpiozero import Button
import slack_notify
from time import sleep
import subprocess

# 初期化
SW_GPIO = 21

# ButtonクラスでGPIOピンを指定し、プルアップを指定
button = Button(SW_GPIO, pull_up=True)

# 写真のパスとファイル名
PHOTO_PATH = '/home/pi/image.jpg'

# 状態の初期化
old_state = button.is_pressed

# 無限ループ
while True:
    try:
        # 現在の状態を取得(False: ボタンが押されている, True: ボタンが離されている)
        new_state = button.is_pressed

        # 条件分岐
        if old_state and not new_state:
            pass
            # print('Close')
        elif not old_state and not new_state:
            pass
            # print('Open')
        elif old_state and new_state:
            pass
            # print('Close->Open')
        elif not old_state and new_state:
            try:
                # 写真を撮影し、LINEへ送信
                subprocess.run(f'fswebcam -r 640x480 -F 1 -S 0 {PHOTO_PATH}', shell=True)
                slack_notify.slack_photo("\n玄関ドアが開いたよ", PHOTO_PATH)  
            except:
                pass
            pass
            # print('Open->Close')

        old_state = new_state
        sleep(0.1)
    except:
        # エラーや強制終了の場合
        print('Err')
        break

# 終了処理
print('End')

Close->Openを検出してsubprocess.runで

fswebcam -r 640x480 -F 1 -S 0 /home/pi/image.jpg

を実行してline.pyのline_photoにslack_notify.pyのslack_photo渡している、ということはまぁ読めばわかる。これによって会社にいても子が学校から帰ってきたのが手元のiPhoneに画像通知が来ることでわかるようになった。もちろん泥棒さんが侵入してもわかるはず(笑)

で、うまく写るように設置を工夫する。百均で買ったカゴにラズパイZEROとUSBカメラを放り込んで吊り下げて設置、ここにBME280もあるし水温測定もさせているというわけだ。再起動しても自動実行するようにcrontabに登録。

@reboot  /usr/bin/python /home/pi/slack_door_zero.py

タイトルとURLをコピーしました