鍵の掛け忘れを自動解決!SwitchBot Smart Lock ProとGoogle Homeで玄関の自動施錠&音声通知を実現する方

SwitchBot

「鍵を掛けたかな?」と外出後に不安になった経験はありませんか?
本記事では、SwitchBot Smart Lock ProのAPIをPythonから操作し、30分ごとに玄関の施錠状態を確認して自動で鍵を掛けるシステムの実装方法を解説します。
施錠が完了したらGoogle Homeが「玄関の鍵を掛けました。」と音声で教えてくれるため、家族全員がリアルタイムに施錠完了を知ることができます。

なぜこの機能を作ったのか?

我が家では子供が帰宅後に鍵を掛け忘れることがよくあります。また、外出時に「本当に鍵を掛けたか」が心配になることも多く、セキュリティ面での不安がありました。
Smart Lock Proを導入してアプリから確認できるようにしましたが、わざわざアプリを開くのが手間です。
そこで、30分ごとに自動で施錠状態をチェックし、施錠されていなければ自動で鍵を掛けてGoogle Homeで通知する仕組みを作りました。これで鍵の掛け忘れを自動で解消できるようになりました。

鍵の掛け忘れをゼロにしたい!掛けたらGoogle Homeで教えてほしい!

実現したいこと

  • 30分ごとに玄関の施錠状態を自動チェックする
  • 施錠されていない場合のみ自動で鍵を掛ける(すでに施錠済みなら何もしない)
  • 施錠完了後、Google Homeで「玄関の鍵を掛けました。」と音声通知する
  • 施錠に失敗した場合もGoogle Homeで「確認してください」と通知する

この記事でわかること

  • SwitchBot APIのv1.0とv1.1の違いと使い分け方
  • SwitchBot API v1.1のHMAC-SHA256署名認証の実装方法
  • PythonからSwitchBot Smart Lock Proの施錠状態を取得してロック操作をする方法
  • 施錠完了をGoogle Homeで音声通知する方法
  • crontabで30分ごとに定期実行する方法

必要な準備と用意するもの

ハードウェア
  • Raspberry Pi(例:Raspberry Pi 5)
  • SwitchBot Smart Lock Pro
  • SwitchBot Hub Mini(Smart Lock ProのAPIを使うために必要)
  • Google Home(Nest)
ソフトウェア/サービス
  • Python
    • requests
    • hashlib / hmac / base64 / uuid(v1.1の署名生成に使用)
  • SwitchBot API(v1.0 / v1.1)
    • SwitchBotアプリの開発者向け設定からトークンとシークレットキーを取得する必要あり
  • Google Home通知ライブラリ(自作 googlehome.py)
    • 別記事「Google Homeに任意のフレーズを発話させる方法」を参照

SwitchBot Smart Lock ProのAPIを使うにはHub Mini(またはHub 2)が必要です。Hub Miniがない場合、Smart Lock Proはクラウド経由でのAPI操作ができません。

完成イメージ

30分ごとにcrontabが自動でauto_lock_door.pyを実行します。

  • 施錠済みの場合:何もしない(Google Homeも発話しない)
  • 未施錠の場合:自動で施錠し、施錠確認後にGoogle Homeが発話
    • 施錠成功:「玄関の鍵を掛けました。」
    • 施錠失敗(5回リトライ後も鍵が掛からない場合):「玄関の鍵を掛けようとしましたが掛かりませんでした。確認してください。」

システムの仕組み

  1. crontab:30分ごとに auto_lock_door.py を実行
  2. SwitchBot API v1.0:Smart Lock Proの現在の施錠状態(lockState)を取得
  3. 施錠されていない場合のみ、SwitchBot API v1.1で施錠コマンドを送信
  4. 4秒待機後、最大5回(2秒間隔)施錠完了を確認
  5. 結果(成功/失敗)をGoogle Homeで音声通知

実装のポイント

SwitchBot API v1.0とv1.1の使い分け

SwitchBot APIにはv1.0とv1.1の2種類があります。本実装では目的に応じて使い分けています。

v1.0:デバイスステータスの取得
認証がAPIキーのみでシンプル。施錠状態(lockState)の取得に使用する

v1.1:コマンドの送信(施錠操作)
HMAC-SHA256による署名認証が必要。施錠などのコマンド操作にはv1.1を使用する

SwitchBot公式ではv1.1以降を推奨しています。コマンド操作は特にセキュリティ面でv1.1の署名認証が重要です。

施錠前に施錠状態を確認する

毎回無条件に施錠コマンドを送ると、外出中に帰宅した家族が手動で鍵を開けた直後に再び施錠されてしまう可能性があります。そのため、施錠状態を先に確認し、施錠されていない場合のみ施錠コマンドを送るようにしています。

door_lock_json = switchbot_instance_10.get_device_status(settings['switchbot']['device_id']['front_door_lock'])

if door_lock_json['lockState'] != 'locked':
    # 施錠されていない場合のみ施錠操作を実行
    ...

施錠後のリトライ確認

施錠コマンドを送っても、鍵のモーターが動く時間があるため即座にlockedになりません。4秒待機した後、2秒間隔で最大5回状態確認をリトライします。

事前準備

SwitchBot APIのトークンとシークレットキーを取得する

  1. SwitchBotアプリ(スマートフォン)を開く
  2. 「プロフィール」→「設定」→「開発者向けオプション」をタップ
  3. 「トークン」と「クライアントシークレット」をメモしておく

トークンとシークレットキーは外部に漏れないよう厳重に管理してください。settings.jsonに保存し、プログラム内にハードコードしないようにしましょう。

Smart Lock ProのデバイスIDを確認する

SwitchBotアプリの「デバイスの設定」→「デバイス情報」からデバイスIDを確認できます。
もしくは後述のAPIを実行してデバイス一覧から確認する方法もあります。

settings.jsonの設定

{
    "switchbot": {
        "api": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "secret": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "api_url": "https://api.switch-bot.com/v1.0/",
        "api_url_v11": "https://api.switch-bot.com/v1.1/",
        "device_id": {
            "front_door_lock": "xxxxxxxxxxxx"
        }
    }
}

実装方法

SwitchBot APIライブラリ (switchbot.py)

まず、SwitchBot APIをPythonから扱うためのライブラリを実装します。ApiVersionクラスでv1.0とv1.1を切り替えられるようにしています。

# -*- coding: utf-8 -*-
from pathlib import Path
import sys
PROJECT_ROOT = Path(__file__).resolve()
while PROJECT_ROOT.name != "smarthome":
    if PROJECT_ROOT.parent == PROJECT_ROOT:
        raise RuntimeError("smarthome project root not found")
    PROJECT_ROOT = PROJECT_ROOT.parent
sys.path.insert(0, str(PROJECT_ROOT))
from libs.bootstrap import Bootstrap
bootstrap = Bootstrap(__file__)
settings = bootstrap.settings
# ------------------------------------------------------------
import requests
import json
import time
import hashlib
import hmac
import base64
import uuid
import smarthomelog
from influx.v2 import client as influxclient
from influx.v2 import writer as influxwriter

class ApiVersion:
    VERSION_10 = 'v1.0'
    VERSION_11 = 'v1.1'

def get_header_v11(token, secret):
    """v1.1用のHMAC-SHA256署名ヘッダーを生成する"""
    nonce = str(uuid.uuid4())
    t = int(round(time.time() * 1000))
    string_to_sign = bytes('{}{}{}'.format(token, t, nonce), 'utf-8')
    secret_bytes = bytes(secret, 'utf-8')
    sign = base64.b64encode(hmac.new(secret_bytes, msg=string_to_sign, digestmod=hashlib.sha256).digest())
    return {
        "Authorization": token,
        "sign": str(sign, 'utf-8'),
        "t": str(t),
        "nonce": nonce
    }

header_v11 = get_header_v11(settings['switchbot']['api'], settings['switchbot']['secret'])
header_v10  = {
    "Authorization": settings['switchbot']['api'],
    'Content-Type': 'application/json; charset: utf8'
}

class Switchbot:
    def __init__(self, api_version: ApiVersion = ApiVersion.VERSION_11):
        if api_version == ApiVersion.VERSION_10:
            self.switchbot_api_url = settings['switchbot']['api_url']
            self.header = header_v10
        elif api_version == ApiVersion.VERSION_11:
            self.switchbot_api_url = settings['switchbot']['api_url_v11']
            self.header = header_v11

        self.influx     = influxwriter.Writer()
        self.log        = smarthomelog.Log(__file__, smarthomelog.LogType.SWITCHBOT)
        self.device_list = None

    def _get_devices(self):
        if self.device_list is None:
            url = self.switchbot_api_url + "devices"
            body = json.loads(requests.get(url, headers=self.header).text)['body']
            self.device_list = body['deviceList']
            self.log.write(url)

    def get_device_status(self, device_id):
        """デバイスのステータスを取得する"""
        url = self.switchbot_api_url + "devices/" + device_id + '/status'
        device_status = json.loads(requests.get(url, headers=self.header).text)['body']
        self.log.write(url)
        self.influx.insert_execution_api_log(__file__, influxwriter.LogApiType.SWITCHBOT, device_id, url, None, None, None)
        return device_status

    def execute_command(self, device_id, json_data):
        """デバイスにコマンドを送信する"""
        url = self.switchbot_api_url + "devices/" + device_id + '/commands'
        response = requests.post(url, headers=self.header, json=json_data)
        self.log.write(url)
        self.influx.insert_execution_api_log(__file__, influxwriter.LogApiType.SWITCHBOT, device_id, url, None, None, str(json_data))
        return response

玄関自動施錠プログラム (auto_lock_door.py)

# -*- coding: utf-8 -*-
__group__       = '防犯'
__description__ = '玄関の鍵を自動で閉める'
from pathlib import Path
import sys
PROJECT_ROOT = Path(__file__).resolve()
while PROJECT_ROOT.name != "smarthome":
    if PROJECT_ROOT.parent == PROJECT_ROOT:
        raise RuntimeError("smarthome project root not found")
    PROJECT_ROOT = PROJECT_ROOT.parent
sys.path.insert(0, str(PROJECT_ROOT))
from libs.bootstrap import Bootstrap
bootstrap = Bootstrap(__file__)
settings = bootstrap.settings
# ------------------------------------------------------------
# このプログラムはcrontabから呼び出されます。
# ------------------------------------------------------------
import googlehome
import smarthomelog
import switchbot
import time

try:
    success_log = smarthomelog.Log(__file__, smarthomelog.LogType.SUCCESS)

    # v1.0:状態取得用、v1.1:コマンド送信用
    switchbot_instance_10 = switchbot.Switchbot(switchbot.ApiVersion.VERSION_10)
    switchbot_instance_11 = switchbot.Switchbot(switchbot.ApiVersion.VERSION_11)

    # 玄関の鍵の現在の状態を取得
    door_lock_json = switchbot_instance_10.get_device_status(settings['switchbot']['device_id']['front_door_lock'])

    if door_lock_json['lockState'] != 'locked':
        # 施錠されていない場合のみ処理する
        google_notify = googlehome.Notify(googlehome.GoogleHome.LIVING)

        # 施錠コマンドを送信(v1.1 APIを使用)
        lock_command = {
            "commandType": "command",
            "command": "lock",
            "parameter": "default"
        }
        switchbot_instance_11.execute_command(settings['switchbot']['device_id']['front_door_lock'], lock_command)

        # 鍵のモーターが動く時間を待つ
        time.sleep(4)

        # 施錠完了を確認(最大5回リトライ、2秒間隔)
        max_retry = 5
        for i in range(max_retry):
            door_lock_json = switchbot_instance_10.get_device_status(settings['switchbot']['device_id']['front_door_lock'])
            if door_lock_json['lockState'] == 'locked':
                google_notify.speak('玄関の鍵を掛けました。')
                break
            time.sleep(2)
        else:
            # 5回リトライしても施錠されなかった場合
            google_notify.speak('玄関の鍵を掛けようとしましたが掛かりませんでした。確認してください。')

    success_log.write('処理が正常に終了しました。', __file__)

except Exception as e:
    log = smarthomelog.Log(__file__, smarthomelog.LogType.SYSTEM_ERROR)
    log.write_error(sys.exc_info())

crontabの設定

30分ごとに自動実行するようにcrontabに登録します。

crontab -e
# 30分ごとに玄関の鍵を自動施錠(毎時0分と30分に実行)
0,30 * * * * /home/<username>/Projects/venv/bin/python /home/<username>/Projects/smarthome/core/control/home_security/auto_lock_door.py >> /home/<username>/Projects/smarthome/logs/crontab/auto_lock_door.log 2>&1

コードの解説

v1.1のHMAC-SHA256署名の生成

v1.1ではAPIキーだけでは認証できません。「トークン+タイムスタンプ+ノンス」の文字列をシークレットキーでHMAC-SHA256署名し、Base64エンコードしたものをリクエストヘッダーに付与します。

nonce = str(uuid.uuid4())                         # ランダムなUUID
t = int(round(time.time() * 1000))                # ミリ秒タイムスタンプ
string_to_sign = bytes('{}{}{}'.format(token, t, nonce), 'utf-8')
secret_bytes   = bytes(secret, 'utf-8')
sign = base64.b64encode(
    hmac.new(secret_bytes, msg=string_to_sign, digestmod=hashlib.sha256).digest()
)

署名はリクエストごとに都度生成する必要があります。タイムスタンプとノンスが変わるたびに署名も変わるため、使い回しはできません。本実装ではモジュール読み込み時に1回だけ生成しているため、長時間稼働させる場合はリクエスト前に再生成する必要があります。

施錠コマンドのJSON形式

SwitchBot APIでSmart Lock Proに施錠コマンドを送る場合のJSON形式は下記の通りです。

{
    "commandType": "command",
    "command": "lock",
    "parameter": "default"
}
  • commandTypecommand固定
  • commandlock(施錠) / unlock(解錠)
  • parameterdefault固定

施錠状態の取得と判定

施錠状態はget_device_status()のレスポンスのlockStateフィールドで確認します。

{
    "deviceId": "xxxxxxxxxxxx",
    "deviceType": "Smart Lock Pro",
    "lockState": "locked",
    "doorState": "closed",
    "calibrate": true
}
  • lockStatelocked(施錠済み)/ unlocked(解錠中)/ jammed(引っかかり)
  • doorStateclosed(ドア閉)/ open(ドア開)

for〜else構文によるリトライと失敗検知

Pythonのfor〜else構文を使って、施錠確認のリトライと最終的な失敗検知を実装しています。forループがbreakされずに最後まで実行された場合にelse節が実行されます。

for i in range(max_retry):
    door_lock_json = switchbot_instance_10.get_device_status(...)
    if door_lock_json['lockState'] == 'locked':
        google_notify.speak('玄関の鍵を掛けました。')
        break           # 施錠確認できたらループを抜ける
    time.sleep(2)
else:
    # breakされずにループが終了した = 施錠確認できなかった
    google_notify.speak('玄関の鍵を掛けようとしましたが掛かりませんでした。確認してください。')

Pythonのfor〜elsebreakが実行されなかった場合にelse節が実行されます。「施錠確認できたらbreak、できなければelse」という意図が明確に表現できるため、リトライ処理の実装に適しています。

動作確認

まずはコマンドラインから手動で実行して動作を確認します。

python /home/<username>/Projects/smarthome/core/control/home_security/auto_lock_door.py

下記の順番で動作を確認してください。

  1. 鍵が施錠済みの状態で実行 → Google Homeが発話しないことを確認
  2. 鍵を手動で解錠した状態で実行 → 自動で施錠され「玄関の鍵を掛けました」とGoogle Homeが発話することを確認

うまく動作しない場合は下記を確認してください。
・settings.jsonのAPIキー・シークレット・デバイスIDが正しいか
・SwitchBot Hub Mini(またはHub 2)が正常に動作しているか
・Smart Lock ProがHub Miniに接続されているか(Bluetoothペアリング済みか)
・APIのリクエスト制限(1日10,000回)に達していないか

手動実行で問題なければ、crontabが正しく設定されていることを確認します。

# crontabの設定確認
crontab -l | grep auto_lock_door

# ログの確認(毎時0分・30分実行後に追記される)
tail -f /home/<username>/Projects/smarthome/logs/crontab/auto_lock_door.log

まとめ

今回はSwitchBot Smart Lock ProのAPIをPythonから操作し、30分ごとに自動施錠してGoogle Homeで通知する仕組みを実装しました。
実装のポイントをまとめると下記の通りです。

  • SwitchBot APIは状態取得(v1.0)とコマンド送信(v1.1)で使い分ける
  • v1.1のHMAC-SHA256署名は「トークン+タイムスタンプ+ノンス」をシークレットキーで署名して生成する
  • 施錠前に施錠状態を確認し、解錠中の場合のみ施錠コマンドを送ることで不要なAPI呼び出しを防ぐ
  • Pythonのfor〜else構文でリトライ後の失敗検知をシンプルに記述できる
  • crontabで定期実行することで、鍵の掛け忘れを自動で解消できる

「lock」コマンドを「unlock」に変えれば解錠の自動化にも応用でき、帰宅時間に合わせて自動解錠するといった拡張も可能です。セキュリティへの影響が大きい操作なので、活用の際はくれぐれも慎重に設計してください。

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