SwitchBotロックを帰宅時に自動で解錠する　スマホのWiFi接続をトリガにする方法

# 1. はじめに
　SwitchBotロックを解除するのにスマホ出してアプリ開いて・・・と操作するのが面倒だなあと常々思っていました。結局購入してから数週間でオートロックのみ使用、解錠は物理的な鍵で行うという始末。スマートロックをただのオートロックとして使うのはちょっともったいないですね。そこで、なんとかして家に着いたら自動で解錠できるようにしようと考えました。

先にどんな実装をしたか書いておきます。
**・家を出たこと/帰宅したことの検知**
　　スマホと自宅WiFiが切断された状態から、接続状態へ遷移したことを帰宅のトリガとしました。スマホに固定IPを割り当て、自宅LAN内のマイコンサーバからスマホへpingが通るか定期的に確認します。
**・SwitchBotロックの解錠**
　　同マイコンサーバからSwitchBotAPIを操作します。

　これと同じことをしたい人が迷わないように、なぜこの実装をしようと思ったのか（他の方法があるのではないか）、システムの構成と動作フロー、設定・実装方法の順に記載します。

プログラムだけ欲しい人のために以下にGitレポジトリのリンクを貼っておきます。
[Github: SwitchBot_autoUnlock_using_WiFiConnectionTrigger](https://github.com/Miyabi1456/SwitchBot_autoUnlock_using_WiFiConnectionTrigger)


# 2. 先行例と使えそうなアイデアの調査
　スマートロックを帰宅時に自動で解錠する方法は調べるといくつかでてきます。ここで挙げる全ての例は、スマホを持ち歩くことを前提としています。私は普段GPSやBluetoothをオフにしているので、今回はそれらの常時オンなどのバッテリ消費を増加させる手段は避けたいと思います。
　帰宅時の自動解錠をやろうと思った際に、ここにある参考記事で事足りる人も多いと思いますので、手間がかからなさそうな順に書いておきます。


**・SwitchBotアプリを使い、ジオフェンス機能(GPS)をトリガに解錠する方法**
[なちブガジェット: SwitchBotでGPSをトリガーにしてアクションを実行する方法](https://nacchib-gadget.com/how-to-use-gps-trigger-by-switchbot/)
最も手軽な方法です。スマホのGPSを使い、指定した範囲から出入りしたのをトリガにします。
しかし、スマホのバッテリ消費が激しくなりそうなのでできればGPS常時オンは避けたいです。

**・IFTTTを使い、スマホのWiFi接続をトリガに解錠する方法**
　[SESAME:【IFTTT連携】自宅のWi-Fiに繋がったらセサミを解錠・施錠！OS1(Android用)](https://ameblo.jp/candyhouse-inc/entry-12379983065.html)
泣きました。私はiPhoneユーザーで、SwitchBotユーザーです。
IFTTTではiPhoneのWiFi接続をトリガにできない上、SwitchBotロックの解錠もできません。
ただ、GPSの代替手段としてスマホがWiFiに接続されたのをトリガとするのはなかなか良さそうです。

**・iOSのショートカットを使い、スマホのWiFi接続をトリガに解錠する方法**
[jabrek: SwitchBotのスマートロックをショートカットから解錠](https://jabrek.net/smart_lock/)
[virtualiment: iOSでWiFiに接続したらショートカットを実行するオートメーションを作成する](https://virment.com/record-location-using-ios-automation-triggered-wifi/)
この2つの参考記事を合わせれば目的が達成できそうです。しかしながら、iOSのショートカットでWiFi接続をトリガにした場合、「実行の前に尋ねる」オプションが表示されず、毎回確認画面をタップする必要があり面倒でした。

# 3. システムの決定
　スマホのバッテリ消費を抑えるため、GPSやBluetoothを用いずWiFi接続をトリガとする方法は良さそうです。IFTTTやiOSのショートカットに頼らずにこれを実現する方法を考えます。以下の2点について考えます。

**・スマホがWiFi接続されたことの検知（家を出たこと/帰宅したことの検知）**
　簡単に思いつくのは、定期的に自宅LAN内からスマホに対してpingが通るか確認することです。今回はこれでいきます。
**・SwitchBotロックの解錠**
　SwitchBot API v1.1からロックの解錠が可能になっています。これを使います。

システムの構成概略図を示します。サーバーにマイコンを使っているのは省エネのためです。マイコンからSwitchBot APIを操作するのは面倒が多いので、電力を無視すればミニPCなどをサーバーにしたほうが楽だと思います。

**使用機材：**
　マイコン　　　：[XIAO ESP32S3](https://wiki.seeedstudio.com/xiao_esp32s3_getting_started/)
　スマートフォン：iPhone SE3
　スマートロック：SwitchBotロック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/75d99ae0-7701-a550-56d9-8963b92a1d3b.png)

ロック解除までの大まかな動作の流れを示します。実際にはこれを無限ループさせます。
マイコンは帰宅/外出状態を保持しており、pingが通るか否かでその状態を更新します。そして外出→帰宅となった場合のみ解錠します。マイコン起動直後は帰宅状態を保持させておきます。

![2023_0929_ロック解除.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/53144471-9e04-0b1c-dc9c-411afe77ab81.png)

ここから準備・実装していきます。

# 4. 準備・設定

## 4-1. スマートフォンに固定IPを割り当てる
　ころころIPが変わると面倒なのでルータの管理画面からスマートフォンに固定IPを割り当てます。割り当てたIPはメモっておきます。

## 4-2. マイコンにMicroPythonファームウェアを書き込む
　今回使用するXIAO ESP32S3のMicroPythonファームウェアはこのページ([Seed Studio: MicroPython for XIAO ESP32S3 Sense(Camera, Wi-Fi)](https://wiki.seeedstudio.com/XIAO_ESP32S3_Micropython/))冒頭の「Software Preparation」->「Firmware and Sample Code」から手に入ります。

ファームウェアを書き込む手順は公式通りですが、PCにESP32S3を1つしか繋いでいない場合はポートの指定が不要となるため、簡略化したコマンドを記載しておきます。

(1)esptoolをインストールする

```powershell
pip install esptool
```

(2)マイコンのフラッシュメモリを消去する

```powershell
esptool erase_flash
```

(3)MicroPythonファームウェアをマイコンに書き込む
先程ダウンロードしたファームウェアのzipファイルを解凍し、そのフォルダ内の「firmware.bin」がある場所へ作業ディレクトリを変更してから以下を実行します。

```powershell
esptool --baud 460800 --before default_reset --after hard_reset --chip esp32s3  write_flash --flash_mode dio --flash_size detect --flash_freq 80m 0x0 firmware.bin 
```

公式のガイドに従い、マイコンへのプログラムの書き込みはThonny IDEを使用します。

## 4-3. SwitchBot APIを操作するためのトークンを取得する
　公式ドキュメントに従います。[SwitchBot: トークンの取得方法](https://support.switch-bot.com/hc/ja/articles/12822710195351-%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AE%E5%8F%96%E5%BE%97%E6%96%B9%E6%B3%95)
トークンとクライアントシークレットをメモしておきます。

## 4-4. SwitchBotロックのデバイスIDを取得する
　API叩いて取得しないといけないのかと思いきや、SwitchBotアプリから「ロック」->「設定（右上の歯車アイコン）」->「デバイス情報」に書いてあります。XX:XX:XX:XX:XX:XXの書式です。
参考：[宇宙行きたい: SwitchBot の DeviceID は Bluetooth アドレスと一緒](https://yoshiori.hatenablog.com/entry/2021/02/27/201049)

# 5. 実装
以下の5つに分けて実装します。

| ファイル名                    | 説明                            |
|--------------------------|-------------------------------------|
| wifi.py                         | マイコンをWiFiに接続する      |
| detect_phone_connection.py   | スマホにpingを飛ばしてWiFi接続状態を確認する|
| switchbot.py                  | SwitchBotロックを解除する |
| ntp.py                  | マイコンの時刻合わせ |
| boot.py                         | マイコン通電時に自動実行されるプログラム |


## 5-1. マイコンをWiFiに接続する
ESP32 MicroPythonドキュメントに従います。[MicroPython1: Quick reference for the ESP32](https://docs.micropython.org/en/latest/esp32/quickref.html#networking)
自宅WiFiをSSIDとパスワードを入力し、Thonny IDEから実行するとこんな感じになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/bcb1c86b-3c6a-d763-78de-0114a3c117ce.png)


```python
import network

#自宅WiFiをSSIDとパスワードを入力
ssid = ""
key = ""

def do_connect():

    sta_if = network.WLAN(network.STA_IF)
    if not sta_if.isconnected():
        print('connecting to network...')
        sta_if.active(True)
        sta_if.connect(ssid, key)
        while not sta_if.isconnected():
            pass
    print('network config:', sta_if.ifconfig())

if __name__ == "__main__":
    do_connect()
```

## 5-2. スマホにpingを飛ばしてWiFi接続状態を確認する
MicroPythonの標準モジュールではpingを飛ばせないため、以下のモジュールをマイコンに追加します。

[Qiita: uPyCraftを用いたESP32のpingコマンドの方法](https://qiita.com/c0117183/items/cdf47b263ccce6867c86)
[MicroPython Forum (Archive): uPing - Ping library for MicroPython](https://forum.micropython.org/viewtopic.php?t=5287)

Thonny IDEのツールバーから「表示」->「ファイル」を選択します。
左にファイルツリーが表示されたら「新しいディレクトリ」から「lib」ディレクトリを作成します。
libディレクトリ内にgitの「uping.py」を追加します。

以下のスクリプトを作成しました。
実行するとこんな感じになります。これでスマホがWiFi接続されているか確認できるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455608/3f05c75f-fa33-2219-471c-48c07346f1c3.png)


```detect_phone_connection.py
import uping
host = "" #スマートフォンのIPアドレスを入力

def detect_connection():
    connected = False
    timeout = 2.0 #s
    
    send, rcv = uping.ping(host, count=5, timeout=timeout*1000, interval=10, quiet=False, size=64)
    
    if rcv > 0: # 1つでも届けば接続状態とする
        print("スマホ接続")
        connected = True
    else:
        print("スマホ未接続")
    return connected

if __name__ == "__main__":
    detect_connection()
```




## 5-3. SwitchBotロックを解除する
過去にSwitchBot APIをマイコンから操作するのはやったことがあります。こちらと同じ環境を構築します。
[Qiita: マイコンボードESP32でMicroPython環境構築からSwitchbotAPI操作まで](https://qiita.com/Miyabi1456/items/52dee7f076ba0064ebb5)

以下の参考ページをもとに実装しました。
[Qiita: Switchbot API v1.1が登場 APIでSwitchBotロックを遠隔操作する](https://qiita.com/masaki12-s/items/0295a96fd5a70442f7d5)

このスクリプトを実行すると、SwitchBotロックが解錠されるか確認できます。
後々使うかもしれないのでロックと扉の状態を取得できる関数も追加しておきました。

```switchbot.py
import time
import hashlib
import hmac
import base64
import urequests

# トークンとシークレットキーを入力する
token = ""
secret = ""

lock_deviceid = "XXXXXXXXXXXX" #SwitchBotロックのデバイスIDを入力する

host_domain = "https://api.switch-bot.com"
ver = "/v1.1"

def get_auth_header(token, secret):
    nonce = '' #空欄のままで良いらしい
    t = int(round((time.time() + 946684800) * 1000)) #UNIXとESP32のエポック基準時刻を合わせるために+946684800
    string_to_sign = '{}{}{}'.format(token, t, nonce)

    string_to_sign = bytes(string_to_sign, 'utf-8')
    secret = bytes(secret, 'utf-8')

    sign = base64.b64encode(hmac.new(secret, msg=string_to_sign, 
    digestmod=hashlib.sha256).digest())
    #print ('Authorization: {}'.format(token))
    #print ('t: {}'.format(t))
    #print ('sign: {}'.format(str(sign, 'utf-8')))
    #print ('nonce: {}'.format(nonce))

    header={}
    header["Authorization"] = token
    header["sign"] = str(sign, 'utf-8')
    header["t"] = str(t)
    header["nonce"] = nonce
    return header

def get_device_list():
    #デバイスの一覧を取得する。APIが正しく操作できるか確認する用
    header = get_auth_header(token, secret)
    response = urequests.get(host_domain + ver + "/devices", headers=header)
    return_json = response.json()
    if return_json["message"] == "success":
        print("取得成功")
        return return_json["body"]
    elif return_json["message"] == "Unauthorized":
        print("認証エラー")
        return None
    else:
        print("エラー")
        return None

def lock():
    header = get_auth_header(token, secret)
    devices_url = host_domain + ver +"/devices/" + lock_deviceid + "/commands"
    data={
            "commandType": "command",
            "command": "lock",
            "parameter": "default",
        }
    try:
        # ロック
        res = urequests.post(devices_url, headers=header, json=data)
        print(res.text)
    except Exception as e:
        print("error:",e)
        
def unlock():
    header = get_auth_header(token, secret)
    devices_url = host_domain + ver +"/devices/" + lock_deviceid + "/commands"
    data={
            "commandType": "command",
            "command": "unlock",
            "parameter": "default",
        }
    try:
        # アンロック
        res = urequests.post(devices_url, headers=header, json=data)
        print(res.text)
    except Exception as e:
        print("error:",e)

def lock_status():
    #ロックとドアの状態を返す
    header = get_auth_header(token, secret)
    lockState, doorState = None, None
    try:
        res = urequests.get(host_domain + ver +"/devices/" + lock_deviceid + "/status",headers=header)
        lockState = res.json()["body"]["lockState"]  #"locked" / "unlocked"
        doorState = res.json()["body"]["doorState"]  # "closed" / "opened"
    except Exception as e:
        print("error:",e)
    return lockState, doorState

def test():
    device_list = get_device_list()
    print(device_list)
    lockState, doorState = lock_status()
    print(f"ロック:{lockState}  ドア{doorState}")
    
    if lockState == "locked":
        unlock()

if __name__ == "__main__":
    test()
```

## 5-4. マイコン通電時に自動実行されるプログラム
フリーズしたときのためにウォッチドッグタイマーとセットするとか、例外処理を色々やったほうがいいかもしれません。とりあえず動くものを作りました。

スマホのWiFiをオンオフしてSwitchBotロックが解錠されることを確認しました。これで完成です。

```boot.py
import time
import ntp
import wifi
import detect_phone_connection
import switchbot

interval_time = 2.0 #sec スマホの接続確認をする時間間隔

def main():
    in_area = True #帰宅/外出状態を保持する
    wifi.do_connect() #マイコンを自宅WiFiに接続
    ntp.set_ntptime() #時計合わせ
    
    while True:
        connected = detect_phone_connection.detect_connection() #スマホへpingを送信してWiFi接続状態の確認
    
        if connected and not in_area: #外出→帰宅したときのみ解錠する
            switchbot.unlock()
        
        #帰宅/外出状態の更新
        if connected:
            in_area = True
            time.sleep(60.0) #家の中にいるときにそれほど高頻度にチェックする必要はない
        else:
            in_area = False
        
        time.sleep(interval_time)
    

if __name__ == "__main__":
    while True:
        try:
            main()
        except Exception as e:
            print(e)
            time.sleep(10.0)
```

# 6. まとめ
　スマホのWiFi接続をトリガとして、SwitchBotロックを帰宅時に自動で解錠するシステムを作りました。ただし、うちの環境だと鉄筋コンクリート造のためか扉の前に立ってもWiFiに繋がりませんでした。

お　　わ　　り


実家の鉄骨造だと外までWiFi繋がるので、使える人は使えると思います。

　
　私の環境ではロックの解錠に使えませんでしたが、WiFiの切断は検知できるので外出時にエアコンや電気を消すなどには使えると思います。
　これに近い機能を持つSwitchBotの製品の一つに開閉センサーがありますが、宅配の受け取りなどで扉を開けて部屋に戻るシーンでも外出扱いになってしまいます。本システムであればそのような誤検知は起こらず便利なのではないかと思います。