オートロック機能付き自作スマートロック(おうちハック)


[OpenStandia](https://openstandia.jp) Advent Calendar 4日目です。ネタはOSS関連であれば何でも良いとのことなので、趣味のおうちハックネタを書こうと思います。

# 現在の我が家のおうちハック全体像

![all-drawio.png](https://qiita-image-store.s3.amazonaws.com/0/13224/dbe0514c-ec2b-8c82-1cc4-4aa4b3fc04a3.png)

1. Apple HomeKitをエミュレートするOSSの[Homebridge](https://github.com/nfarina/homebridge)により各種デバイス等をiPhoneから操作
 - 操作対象のデバイスの中には、既成品で専用アプリがあるものもあるが、アプリが分散するのが嫌なのでApple Homeアプリで操作できるようにしている

1. Bluetoothによる在宅検知
 - 特定のMacアドレスを持つ端末が近くにいる場合在宅と判定
 - Apple Homeアプリに位置情報を元にした自動操作機能はあるが、GPS情報を利用しているため帰宅等を判定する距離が広いので使い分けている
 - WiFiで自宅ネットワークに接続された端末へのPingでの判定も考えたが、WiFiの場合、端末によってはWiFiモジュールがスリープしてしまうのか、一定時間立つとPing届かなくなり不在と判定してしまうことがあったため、この方式とした
1. [Raspberry Pi Zero W](https://www.raspberrypi.org/products/raspberry-pi-zero-w/)と[サーボモータ](http://akizukidenshi.com/catalog/g/gM-08761/)、[距離センサ](http://akizukidenshi.com/catalog/g/gM-11009/)による玄関のスマートロック
 - Bluetoothによる在宅検知状況により自動操作も行う
 - 構築時は我が家のサムターンに合う市販のスマートロック機器がなかったため自作した
 - 今回はこのスマートロックについて詳細を説明する
1. [SwitchBot](https://www.switchbot.jp/bot)と[SensorTag](http://www.tij.co.jp/ww/wireless_connectivity/sensortag/)による集合玄関のインターホンの自動操作
 - Apple Homeアプリで自宅に近づいたことを検知し、インターホンのモニターをSensorTagの照度センサで監視、集合玄関からの呼び出しによるモニター点灯後SwitchBot複数台で解錠操作
1. SwitchBotによる空気清浄機のジェットモード操作
 - Bluetoothによる在宅検知状況により自動操作も行う
1. 赤外線リモコン[Nature Remo](https://nature.global)によるエアコンや照明等の家電操作
 - 専用アプリだけでなくHomebridgeを利用してApple Homeアプリでも操作可能にしている
 - Bluetoothによる在宅検知状況により自動操作も行う
1. AWS IoT ボタンでの就寝時の一括家電オフ
 - 個人的にはスマートスピーカーでは喋らないといけないのが嫌なため、IoTボタンを活用(スマートスピーカーは指示待ちスピーカーであってスマートではないと思っています)
 - 本当はセンサ等で寝に行くことを検知したい

# オートロック機能付き自作スマートロック

今回は特に自作度の高い玄関のスマートロックについて説明します。
[Raspberry Pi Zero W](https://www.raspberrypi.org/products/raspberry-pi-zero-w/)と[サーボモータ](http://akizukidenshi.com/catalog/g/gM-08761/)、[距離センサ](http://akizukidenshi.com/catalog/g/gM-11009/)を利用しています。

実物はこんな感じになっています。
![smartlock-callout.jpg](https://qiita-image-store.s3.amazonaws.com/0/13224/6ab18640-badc-4fe9-14f4-9169b0fbc69b.jpeg)

動作している様子です。
![smartlock.gif](https://qiita-image-store.s3.amazonaws.com/0/13224/a64e9269-21f1-e46a-1c65-3cac0e3bcf03.gif)

構成機能としては、サーボモータによる解錠機能、距離センサによるオートロック機能、ボタンによる解錠機能に分かれるので、それらに分けて説明していきます。
なお、本スマートロックのプログラムはPythonで作成しています。
また、それぞれの機能をDockerコンテナ化して運用しているので、利用パッケージの紹介も兼ねてDockerfileも載せます。

骨組みはタミヤの[工作用品](https://tamiyashop.jp/shop/c/c401030/)のユニバーサルアームを使用しています。

## サーボモータによる解錠
本スマートロックのメインのサムターン操作部分です。サーボモータは操作対象のサムターンによって、トルクの大きさで選ぶ必要があるかと思います。
本スマートロックでは[SG-90](http://akizukidenshi.com/catalog/g/gM-08761/)を使っています。

回路図やコードは以下のとおりです。

### 回路図
サーボモータはVccとGNDとRaspberry Piからの制御信号の入力のみなので、単純な回路となっています。
![key.png](https://qiita-image-store.s3.amazonaws.com/0/13224/7039ca80-c78e-8b01-a0f9-0359dbd5ce17.png)

### ソースコード
まず、Dockerfileです。
gccやmusl-devはpipでのパッケージインストール時のビルドのために入れています。

```dockerfile:Dockerfile
FROM python:3.7.1-alpine3.8

RUN apk update && \
    apk --no-cache add gcc musl-dev && \
    pip install --upgrade pip && \
    pip install flask rpi.gpio

ADD app /app

CMD ["python", "/app/main.py"]
```

プログラムのコードです。
サーボモータ操作部分です。PythonでのRaspberry PiのGPIOを使った処理の実装はRPi.GPIOパッケージを利用することで簡単に行えます。

```python:app/key.py
import RPi.GPIO as GPIO
import time

SERVO_GPIO = 18 #Raspberyy Piのサーボモータと接続詞ているGPIOのピン番号
SERVO_PWM = 50 #サーボモータのPWMサイクル値(SG-90の場合、20ms=50Hz)
SERVO_OPEN = 2.5 #解錠時にサーボモータに渡すデューティサイクル値(鍵に合わせて変更)
SERVO_CLOSE = 7.5 #施錠時にサーボモータに渡すデューティサイクル値(鍵に合わせて変更)
status = ""

GPIO.setmode(GPIO.BCM)
GPIO.setup(SERVO_GPIO, GPIO.OUT)

def control_key(dc):
  servo = GPIO.PWM(SERVO_GPIO, SERVO_PWM)
  servo.start(0.0)
  servo.ChangeDutyCycle(dc)
  time.sleep(0.3) #モータが動き終わるまで待ち
  servo.stop()

def open_key():
  global status
  control_key(SERVO_OPEN)
  status = "open"
  return status

def close_key():
  global status
  control_key(SERVO_CLOSE)
  status = "close"
  return status

def get_status():
  return status

```

今回は各機能のコンテナ間はHTTPリクエストでやり取りを行うようにするため、RESTっぽいAPIをFlaskで用意しました。

``` python:app/main.py
from flask import Flask, jsonify, abort, make_response
import key

api = Flask(__name__)

@api.route('/controlKey/<string:controlId>', methods=['GET'])
def control_key(controlId):
  if controlId == "status":
    status = key.get_status()
  elif controlId == "open":
    status = key.open_key()
  elif controlId == "close":
    status = key.close_key()
  else:
    return make_response(jsonify({'error': 'Not found'}), 404)

  result = {
    "result":True,
    "data":{
      "controlId":controlId,
      "status":status
    }
  }

  return make_response(jsonify(result))

@api.errorhandler(404)
def not_found(error):
  return make_response(jsonify({'error': 'Not found'}), 404)

if __name__ == '__main__':
  api.run(host='0.0.0.0', port=80)
```

## 距離センサによるオートロック
扉が閉まったことを検知してオートロックさせます。
本スマートロックでは距離センサを用いて壁までの距離によって閉まったことを検知するようにしました。
距離センサは超音波式の[HC-SR04](http://akizukidenshi.com/catalog/g/gM-11009/)というものを利用しました。

回路図やコードは以下のとおりです。

### 回路図
距離センサはサーボモータと違いセンサ情報のRaspberry Piへの入力があります。
Raspberry Pi のGPIO入力は3.3Vである必要があるようなので、抵抗により分圧しています。
![sensor.png](https://qiita-image-store.s3.amazonaws.com/0/13224/e3ad389b-3330-2514-a4f2-0273fb65a5c1.png)


### ソースコード
Dockerfileです。
サーボモータのDockerfileと同様に、gccやmusl-devはpipでのパッケージインストール時のビルドのために入れています。


```dockerfile:Dockerfile
FROM python:3.7.1-alpine3.8

RUN apk update && \
    apk --no-cache add gcc musl-dev && \
    pip install --upgrade pip && \
    pip install requests rpi.gpio

ADD app /app

CMD ["python", "/app/main.py"]
```

次にプログラムのコードです。

`read()`がセンサで距離を取得する関数です。
[HC-SR04](http://akizukidenshi.com/catalog/g/gM-11009/)の場合、Trigピンに10us以上Highレベルパルスを流すことで距離計測が始まります。
その後、超音波の送信から受信までの間、EchoピンがHighレベルとなります。
距離はそのEchoピンがHighレベルであった時間の半分の値(片方向の時間)と音速から得られます
詳細は[秋月電子さんのHC-SR04のページ](http://akizukidenshi.com/catalog/g/gM-11009/)にもあるデータシートに記載されていますのでそちらをご確認ください。

今回は解錠されたら距離取得を開始し、距離に応じて扉が開かれたかを判定しています。
扉が開かれた後、再び閉められているであろう距離に戻った場合、施錠します。
また、解錠された後、一定時間扉が開かれない場合も自動で施錠するようにしました。

なお、サーボモータ操作機能へのAPIアクセス時のホスト名はDockerのlink機能を利用する想定です。

```python:app/main.py
import requests
import time
import RPi.GPIO as GPIO

CLOSE_TIME = 0.5 #扉が閉まったと判定するまでの時間
CLOSE_TIMEOUT = 120 #解錠されてから扉が開けられないまま放置された場合の自動施錠までの時間
CLOSE_DISTANCE = 12 #扉が閉まっていると判定する距離

GPIO.setmode(GPIO.BCM)
SENSOR_TRIG = 17
SENSOR_ECHO = 27
GPIO.setup(SENSOR_TRIG, GPIO.OUT)
GPIO.setup(SENSOR_ECHO, GPIO.IN)
GPIO.output(SENSOR_TRIG, GPIO.LOW)

KEY_URL = 'http://key/controlKey/'

def read():
  GPIO.output(SENSOR_TRIG, True)
  time.sleep(0.00001)
  GPIO.output(SENSOR_TRIG, False)

  t = time.time()
  signaloff = time.time()
  while GPIO.input(SENSOR_ECHO) == 0:
    signaloff = time.time()
    if time.time() > t + 10: #プログラムよりセンサの処理が早く、Echoの取得失敗時のタイムアウト
      break

  signalon = time.time()
  while GPIO.input(SENSOR_ECHO) == 1:
    signalon = time.time()

  distance = (signalon - signaloff) * 17000
  return distance

def check_key_close():
  while True:
    time.sleep(0.5)
    t = time.time()
    while read() <= CLOSE_DISTANCE and read() > 2: #2以下は異常値とし無視
      time.sleep(0.2)
      if time.time() > t + CLOSE_TIME:
        requests.get(KEY_URL + 'close')
        return

def check_key_status():
  r = requests.get(KEY_URL + 'status').json()
  return r['data']['status']

if __name__ == '__main__':
  while True:
    time.sleep(1)
    t = time.time()

    while check_key_status() == 'open':
      time.sleep(1)

      if time.time() > t + CLOSE_TIMEOUT:
        requests.get(KEY_URL + 'close')

      if read() > CLOSE_DISTANCE * 1.5:
        check_key_close()
```

## ボタンによる解錠
解錠はApple Homeアプリでの操作やBluetoothでの帰宅検知時といったAPI経由の他に、物理ボタンも用意しました。
ボタンは家にあったものを適当に利用しましたが、おそらく[このあたりのスイッチ](https://www.amazon.co.jp/uxcell-a13040500ux0239-%E3%82%BF%E3%82%AF%E3%83%88%E3%82%B9%E3%82%A4%E3%83%83%E3%83%81-%E9%87%91%E5%B1%9E-%E9%9B%BB%E5%AD%90%E9%83%A8%E5%93%81-%E3%83%96%E3%83%A9%E3%83%83%E3%82%AF-10%E5%80%8B%E5%85%A5%E3%82%8A/dp/B00EDJYK46)です。

回路もコードも単純です。

### 回路図
今回はプルアップとしたので、GPIOポートとGNDにスイッチを接続しています。
![button.png](https://qiita-image-store.s3.amazonaws.com/0/13224/f5cd86be-6c36-0be1-19ad-f7eaccb04379.png)

### ソースコード
Dockerfileです。
距離センサのものと変わりません。

```dockerfile:Dockerfile
FROM python:3.7.1-alpine3.8

RUN apk update && \
    apk --no-cache add gcc musl-dev && \
    pip install --upgrade pip && \
    pip install requests rpi.gpio

ADD app /app

CMD ["python", "/app/main.py"]
```

次にプログラムです。
ボタンが押下されるまで待ち、押下されたら解錠するだけの単純なものです。
RPi.GPIOでは`wait_for_edge()`で立ち上がりや立ち下がりを待つことができます。
距離センサと同様に、サーボモータ操作機能へのAPIアクセス時のホスト名はDockerのlink機能を利用する想定です。

```python:app/main.py
import requests
import time
import RPi.GPIO as GPIO

BUTTON_GPIO = 24

GPIO.setmode(GPIO.BCM)
GPIO.setup(BUTTON_GPIO, GPIO.IN, pull_up_down=GPIO.PUD_UP)

KEY_URL = 'http://key/controlKey/'

if __name__ == '__main__':
  while True:
    GPIO.wait_for_edge(BUTTON_GPIO, GPIO.FALLING)
    requests.get(KEY_URL + 'open')
    time.sleep(2)
```

## Docker Composeでの実行
最後にDockerコンテナ実行時のオプションの紹介も兼ねて、3つのDockerコンテナを実行するためのDocker ComposeのYAMLファイルも載せておきます。
devicesやcap_addはコンテナからGPIOへアクセスするための設定です。`wait_for_edge()`が、原因は追えていませんが、それらだけでは利用できなかったため、buttonコンテナはprivilegedをtrueとしてしまっています。

```yaml:docker-compose.yml
version: '3'
services:
  key:
    build: key
    ports:
      - 80:80
    devices:
      - /dev/mem
    cap_add:
      - SYS_RAWIO

  sensor:
    build: sensor
    links:
      - key
    devices:
      - /dev/mem
    cap_add:
      - SYS_RAWIO

  button:
    build: button
    links:
      - key
    privileged: true
```

なお、ディレクトリ構成は下記のような構成の想定です。

```:tree
├─ docker-compose.yml
├─ button
│  ├─ Dockerfile
│  └─ app
│     └─ main.py
├─ key
│  ├─ Dockerfile
│  └─ app
│     ├─ key.py
│     └─ main.py
└─sensor
   ├─ Dockerfile
   └─ app
      └─ main.py
```


---
以上、自作スマートロックの紹介でした。
Raspberry Piとサーボモータを使ったスマートロックを実装されている方は多いですが、距離センサを使ったオートロックを行ってる人は見かけたことがないので、誰かの参考になればと思います。

なお、構築時は我が家のサムターンに合う市販のスマートロック機器がなかったため自作しましたが、今はQrioの二世代目のスマートロック機器の[Qrio Lock](https://qrio.me/smartlock/)が我が家でも使えるため、併用してみています。
今回の自作スマートロックは見た目や電源の取り回しがデメリットで、良い市販品が出てきたら乗り換えようと考えていましたが、Qrio Lockは帰宅検知の精度やオートロック速度が自作版に比べると劣っており、乗り換えるか悩んでいます。