Smart HomeをDIYする

Apple HomeKit は対応製品が少なくて高価なのでHomebridgeでがんばります

macOSターミナルからHomeKitを使う

macOSのショートカットアプリを使うと、HomeKitアクセサリをmacOSのターミナルからも操作可能です。これにより、照明を点灯・消灯する、エアコンを稼働するなどのHomeKit操作が、シェルスクリプトやさまざまなプログラム言語から実行できます。

Mac版ショートカットを使う

ショートカットは、元々はiPhoneのアプリでした。ショートカットは、iPhoneの一連の操作をまとめて一つのアイコンにしてくれます。このアイコンをiPhoneの画面に置けば、アイコンをタップするだけで自動実行できます。複数ステップで行う操作を、タップ1回で近道できるという意味で、ショートカットと名付けられてます。

iPhoneアプリだったショートカットが、macOS 12.0 MontereyからmacOSでも動くようになりました。macOSでは、完成したショートカットアイコンがファインダーに並ぶことはありませんが、常時表示されるサービスメニュー(アプリケーション共通のメニュー)やメニューバーにショートカットを登録できます。ユーザはいつでもこのメニューを選択できて、ショートカットを実行できます。

ショートカットからHomeKitを使う

ショートカットは、macOSと対応アプリケーションの機能を呼び出すことができます。Macのショートカットから、LedvanceのLED電球を点灯して、5秒後に消灯する簡単なショートカットを作ってみます。新規ショートカットを選んで、名前をLedvance 5sにして、右側のカテゴリやAppから色々選びます。

  1. 最初に、Appのホームから、XXXをコントロールの項目を選び、LedvanceのLED電球を点灯するステップを作りました。
  2. 次にスクリプティングから、待機を選び5秒に設定し、
  3. 再び電球を選び、今度は消灯するステップを作りました。

これで、5秒だけ点灯させるショートカットを作成できました。

このショートカットは、Macのメニューバーから起動できるだけでなく、iPhoneApple Watchにもすぐに現れて、そちらからも起動できます。MaciPhoneApple IDで連携しているからです。コンピュータとスマホがスムーズに連携する機能は流石の作りです。

コマンドラインからショートカットを使う

Mac版ショートカットの素晴らしいところは、コマンドラインインタフェース(CLI)が用意されているところです。ターミナルアプリからCLIで操作できます。これに使用するのがshortcutsというコマンドです。shortcuts -hでヘルプが出ます。

% shortcuts -h
OVERVIEW: Command-line utility for running shortcuts.

USAGE: shortcuts 

OPTIONS:
  -h, --help              Show help information.

SUBCOMMANDS:
  run                     Run a shortcut.
  list                    List your shortcuts.
  view                    View a shortcut in Shortcuts.
  sign                    Sign a shortcut file.

  See 'shortcuts help ' for detailed help.

shortcuts listコマンドで一覧が表示されます。shortcuts runコマンドで、ショートカットを起動できます。上で作成したLedvance 5sを起動したかったら、

shortcuts run "Ledvance 5s"

とタイプします。ショートカット名にスペースを入れてしまったので、””が必要になってしまいました。これで、シェルスクリプトや各種プログラミング言語からショートカットを起動できます。

ショートカットで標準入力を読む

これで色々なアクセサリをon/offできるのですが、その全ての組み合わせに対して、一つずつショートカットを作っていくと、数が膨大になりそうです。なので、標準入力を読み、その内容に従って複数の動作をするショートカットを考えました。名前は、HomeKitShortcutとしました。内容は以下です。

  1. 最初に標準入力の内容をcommandという変数に設定しています。
  2. 次に、それを文字列比較して、
  3. LEDVANCE_ONという文字列だったらLEDをonして、
  4. LEDVANCE_OFFという文字列だったらLEDをoffにします。

文字列の一致ではなくて、「・・・で始まる」かどうかで判定しているのは、改行コードなどの影響を無視するためです。

コマンドラインからは、送信したい文字列をパイプで送れば良いです。例えば、

echo LEDVANCE_ON | shortcuts run HomeKitShortcut

とすればLED電球を点灯して、

echo LEDVANCE_OFF | shortcuts run HomeKitShortcut

とすればLED電球を消灯できます。crontabに仕込んでおけば、時刻指定の繰り返し実行ができますね。

コマンドラインから呼び出せるので、そのままシェルスクリプトにもできます。例えば以下のシェルスクリプトで、LEDを3秒間onにした後、offにすることができます。

#!/bin/sh
echo LEDVANCE_ON | shortcuts run HomeKitShortcut
sleep 3
echo LEDVANCE_OFF | shortcuts run HomeKitShortcut

同じことはC言語でも:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
  printf("Going to switch the LED\n");
  system("echo LEDVANCE_ON | shortcuts run HomeKitShortcut");
  sleep(3);
  system("echo LEDVANCE_OFF | shortcuts run HomeKitShortcut");
  return 0;
}

Pythonでも:

#!/usr/local/bin/python3
import subprocess
import time
subprocess.run("echo LEDVANCE_ON | shortcuts run HomeKitShortcut", shell=True)
time.sleep(3)
subprocess.run("echo LEDVANCE_OFF | shortcuts run HomeKitShortcut", shell=True)

できます。これでどんなに高度で複雑な処理をするプログラムからでも、HomeKitを呼び出せますね。

ショートカットから標準出力に書く

上の例では、ホームアプリの、「xxxをコントロール」というコマンドを使って、xxxの家の中にあるアクセサリ(上の例ではLedvance RGBという名前のLED電球)を制御しました。ここで、「xxxの状態を取得」というコマンドを選べば、そのアクセサリの状態を取得できます。

例えば、「Ledvance RGBの電源状態を取得する」をショートカットに追加すると、次にウィンドウが開いて、電源状態に合わせて1 (on)か0 (off)が表示されます。

もう少し面白い例をやってみます。手元のHomebridgeに接続されているNature Remoには内蔵の温度計があり、エアコンリモコンからその値を読めます。なので、アクセサリとしてエアコンを指定すると、現在室温を表示させることができます。

シェルコマンドのshortcutsコマンドでは、この出力は標準出力に送られるようです。Pythonプログラムで、シェルコマンドを実行して、標準出力を表示するプログラムを書き、この室温を表示してみます。(このショートカットにgetHomekitという名前を付けてます。)

#!/usr/local/bin/python3
import subprocess
from subprocess import PIPE
proc=subprocess.run("shortcuts run getHomekit", stdout=PIPE, shell=True)
print(proc.stdout)

実行させると以下のようになりました。

% ./getHomekit.py  
b'23.2\xc2\xb0C'

16進数のc2b0に相当する部分は、多分23.2°Cの°を表すunicodeだと思います。これで室温をプログラムで得ることができました。On, offの状態などももちろん得られます。これにより、HomeKitのアクセサリの状態に合わせて、色々な動作をするPythonプログラムを書くことが可能になります。

ショートカットとクリップボード

上記の例ではPythonを使いました。シェルコマンドからはどうでしょうか。ということで、ショートカットをシェルコマンドから呼び出します。

% shortcuts run getHomekit

でもなぜか標準出力が表示されません。良いやり方があるのかもしれませんが、お手上げでした。

そこで、クリップボードを使ってみました。クリップボードは、ペーストバッファとも呼ばれるメモリで、コピーペーストで使われる部分です。「クリップボードにコピー」というコマンドを書いてあげると、

結果がクリップボードに入ります。

% shortcuts run getHomekit

と実行させても、変化はありませんが、この後、コマンドpを押すと、結果が表示されます。

% shortcuts run getHomekit
% 23.3°C

 macOSのシェルコマンドには、pbcopyとpbpasteというコマンドがあります。ペーストバッファへコピーする・ペーストするコマンドです。なので、pbpasteコマンドを実行すれば、クリップボードの内容を標準出力に表示できます。

% shortcuts run getHomekit
% pbpaste
23.3°C

1行で書くなら、;で接続して、

% shortcuts run getHomekit; pbpaste
23.3°C

と書けます。

まとめ

macOSのショートカットアプリはコマンドラインから呼び出せます。またショートカットアプリはHomeKitをコントロールできます。なので、HomeKitはコマンドラインからコントロール可能です。各種プログラミング言語との連携が簡単に作れます。ショートカットアプリは、HomeKitに限らず、様々な機能の自動化を提供します。これがコマンドラインから起動できることで、可能性が広がると思います。

品薄Raspberry Pi 4が買えたのでHomebridgeを引越した

ずっと品切れだったRaspberry Pi 4が入手できたので、

をインストールしました。補助記憶にはUSBメモリを使い、電源とファンも強化しました。

Homebridgeサーバ遍歴

Homebridgeなどのサーバー用に、最初はRaspberry Pi Zero Wを使ってました。実用的に動作しましたが、再起動に時間がかかるので、後にRaspberry Pi 4 (2GB)に移行しました。起動が高速になって快適でした。Homebridgeの使用メモリは0.4GBくらいなので2GBで十分でした。

そのRaspberry Pi 4が、半年前くらいに起動しなくなりました。またSDカードの破損だろうと思ったのですが(SDカードの破損は3回以上経験してます)どうも違うようです。起動プロセスの最後の方で停止してます。ビデオチップの故障のような気がしました。

しかし、半導体不足により品薄になってしまったので、新しいRaspberry Pi 4が買えず、Intel NUCにUbuntuをインストールしてHomebridgeを使ってました。Intel NUCはそれなりに調子良く動いていました。

品薄だけど買えた

そうしていたら、たまたまタイミング良くRaspberry Pi 4を買うことができました。しばらく前からスイッチサイエンスでRaspberry Pi 4 (4GB)の入荷お知らせメールを申し込んでいました。どうせ買い換えるなら、メモリ容量を増やしてみようかと思いました。ただ人気のようで、連絡メールに気づいてもすでに売り切れていることばかりでした。今回は、たまたますぐにメールに気づき、購入できました。4GBモデルの在庫を確認したところ、メールが来てから5分後には品切れになってました。

世の中では、Homebridgeを動かすためにRaspberry Piを使うケースが多いです。Raspberry Piで色々な動作検証をした方が有用なブログ記事になると思い、Homebridgeを引っ越すことにしました。ということでRaspberry Piに出戻りします。

記憶媒体USBメモリ

SDカードが何度も壊れたので、USBメモリを使いました。今までの経験では、USBメモリよりもSDカードの方が壊れやすいと思ってたのですが、ネットで探したところではそのような話は書かれてませんでした。しばらくはUSBメモリを使って、耐久性の問題があれば、次はUSB経由のSATAドライブを試したいと思います。

以前は、Raspberry PiをUSB起動させる設定がすごく面倒でした。でも最近のファームウェアではデフォルトで起動可能になってます。そこで、小型USB 3.0メモリで、信頼できそうなブランド品を探しました。

小型で出っ張りが少なく隣接のUSBソケットの邪魔にならず、それでいて楽に抜き差しできる引っかかりもあります。Raspberry Piの同じサイドにあるUSBコネクタやEthernetコネクタの出っ張りに比べるとかなり短いので、もう少し大きめのUSBメモリでも良かったかもしれません。

SDカードに比べて信頼性が上がると共に、速度にも期待しました。それで、Raspberry Pi用に使用する前に、macOSのAmorphousDiskMarkというソフトでベンチマークしてみました。今まで使用していたTranscendの業務用と言われてた

  • SDカードが、Read 24 MB/s, Write 13 MB/s

だったのに対して、同じ条件で、

でした。時間がかかっていた再起動はほとんどRead動作だと思うので、速度向上が期待できます。

余談ですが、このSDカードのベンチマークはなかなか優秀で、USB 2.0のUSBメモリを使うとここまでの速度は出ません。なので、速度を改善する目的でUSB起動を行うなら、USB 3.0ポートのあるRaspberry Pi 4を使う必要があると思いました。USB 2.0しか搭載していないRaspberry Piだと、速度向上のメリットは無いと思います。SDカードって意外と高速で優秀だったんですね。

このSanDiskUSBメモリAmazonレビューを見ると、大きなサイズのファイルを2分間くらいにわたって書き込み続けると、発熱で速度が落ちると書いてあります。確かに、次に説明するHomebridgeイメージを書き込む作業では、そこそこ熱くなってました。でもHomebridge動作中は、ほんのり温かい程度の発熱です。発熱に関しても、今後の様子を見てみようと思います。

Raspberry Pi Imagerで起動USBを作る

USBメモリにも、SDカードと同様にRaspberry Pi Imagerでインストールできます。

www.raspberrypi.com

Raspberry Pi Imagerの「OSを選ぶ」から、Homebridgeを選びます。

右下の歯車アイコンを押すと、起動の設定ができます。ディスプレイは接続しない予定なので、sshを有効にしておきます。スクショでは選択していませんが、公開鍵認証のみにしておきました。ユーザ名もいつも使っている名前にしました。セキュリティも上がるし、sshするときにユーザ名を入れなくて良いので楽です。またここで、時差を設定しておけばログの日時などが日本時間になるので楽です。Ethernetを使う予定ですが、バックアップとして2.4GHzのWiFiも設定もしました。

「ストレージを選ぶ」からUSBメモリを選んで、「書き込む」ボタンを押せば作成終了です。Homebridgeを書き込んだUSBメモリを入れてRaspberry Piを起動します。

固定アドレスにする

これで、適当なブラウザからhttp://raspberrypi.local:8581を開けば、Homebridgeを使用できます。

デフォルトではアドレスをDHCPで取得してます。MQTTブローカなどを起動するので、固定アドレスにしておきたいです。そこで、Homebridgeの設定を進める前に、固定アドレスにしました。/etc/dhcpcd.confの

# Example static IP configuration:

のコメントのある付近を参考にして書き直します。

# Example static IP configuration:
interface eth0
static ip_address=192.168.xxx.xxx/24
static routers=192.168.xxx.1
static domain_name_servers=192.168.xxx.1 1.1.1.1 1.0.0.1

この後、dhcpcdを再起動したら、固定アドレスで動作しました。

$ sudo systemctl restart dhcpcd

バックアップから復元

ここまで非常に順調でしたので、このままIntel NUCの環境を移行することにしました。なので、Homebridgeのバックアップ機能を使って、今までのHomebridge環境の設定ファイルを新しいHomebridgeに移行しました。このように移行すれば、プラグインの復元と設定、ブリッジのIDなどが全て引き継がれます。HomeKitの再設定も不要です。ユーザ名とパスワードも引き継がれます。

しかしこれで再起動してもエラーが出ます。MQTTブローカとZigbee2MQTTが足りないからです。

Mosquittoのインストール

次にこちらに書いた手順で、MQTTブローカであるMosquittoをインストールしました。

diysmarthome.hatenablog.com

基本的には、aptコマンドでインストールするだけです。

sudo apt install mosquitto mosquitto-clients

また、OS起動時に自動的にMQTTブローカが立ち上がるように以下の設定をしました。

sudo systemctl enable mosquitto.service

Intel NUCの環境では、ユーザアカウント、パスワード付きで動作させてました。同じ設定を実現するために、古い環境(Intel NUCコンピュータ)から

  • /etc/mosquitto/mosquitto.conf
  • /etc/mosquitto/password.txt

の2個の設定ファイルをコピーし、Raspberry Pi 4に複写しました。

Zigbee2MQTTをインストール

次にZigbee2MQTTを、公式ページの手順に従ってインストールしました。

www.zigbee2mqtt.io

HomebridgeにはNode.jsが含まれているかと思ってたのですが、入ってませんでした。のでZigbee2MQTT公式ページの手順に従って、

sudo curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt install -y nodejs git make g++ gcc

としました。apt-getではなく、aptで統一しようと思っているので、aptを使いました。この後も公式ページの手順に従って、zigbee2mqttのディレクトリを作り、ユーザを設定して、

sudo mkdir /opt/zigbee2mqtt
sudo chown -R ${USER}: /opt/zigbee2mqtt

 Zigbee2MQTTのリポジトリをクローンしてディペンデンシーをインストールします(カタカナの意味がちょっとよくわかってないです)。

git clone --depth 1 https://github.com/Koenkk/zigbee2mqtt.git /opt/zigbee2mqtt
cd /opt/zigbee2mqtt
npm ci
npm start

この後、/opt/zigbee2mqtt/data/configuration.yaml ファイルに設定を書きます。公式ページに説明があります。今回は、Intel NUCで設定した内容そのままなのでコピーすることにしました。yamlファイルだけでなく、Zigbeeバイスのデータベースも全て移動します。そのためには、/opt/zigbee2mqtt/data の中のファイルを全部持ってくれば良いです。(実際にはdataディレクトリの内容をtarでまとめて、scpでコピーしました。)これでペアリング済みのZigbeeバイスの情報が引き継がれるので、新たにペアリングする必要はないです。

最後に、systemctlコマンドでサーバとして起動させるために、/etc/systemd/system/zigbee2mqtt.service ファイルを作り、以下の内容にしました。Userの部分は、ログインしているユーザ名にします。Pi Imagerでユーザを指定した場合は、それに書き換えてください。

[Unit]
Description=zigbee2mqtt
After=network.target

[Service]
ExecStart=/usr/bin/npm start
WorkingDirectory=/opt/zigbee2mqtt
StandardOutput=null
StandardError=inherit
Restart=always
RestartSec=10s
User=pi

[Install]
WantedBy=multi-user.target

これで

cd /opt/zigbee2mqtt
npm start

とすると、いろいろ準備をしてくれた後起動するはずです。コントロールCで止めます。また、

sudo systemctl enable zigbee2mqtt.service

とすれば、以後、自動的に起動します。とりあえず起動しておきたいので、

sudo systemctl start zigbee2mqtt.service

としておきます。

Homebridge再構築に必要な作業

今回、Intel NUCで動作しているHomebridgeを、Raspberry Pi 4に移行しました。この際に必要だったインストール作業をまとめておくと以下です。将来何かトラブルがあって、Homebridgeを再構築する必要があったときに役立つかと思います。

  1. Homebridgeをインストールする
  2. Homebridgeを復元する
  3. Mosquittoを再インストールする。
  4. /etc/mosquittoにあるmosquitto.confとpassword.txtを復元
  5. Zigbee2MQTTを再インストールする
  6. /ope/zigbee2mqtt/data以下を復元

ということで、バックアップしておくと良いファイルは、

  • Homebridgeのバックアップアーカイブファイル
  • /etc/mosquitto以下の2ファイル
  • /opt/zigbee2mqtt/data以下の全ファイル

の3種類でした。

Raspberry Piの冷却

基板のままの状態でここまでの作業をしていると、Raspberry Piがとても熱くなってました。手で触ると、熱さでヒリヒリするくらいです。Homebridgeの設定ページの温度表示は60度近くになってます。心なしか動作が低下して、応答が途切れることもありました。不安定になっているようです。

試しに、USB給電の直径10cmくらいの扇風機で微風を当ててみたところ、温度はどんどん低下して37度くらいになりました。冷却は必須のようです。そこで、基板のまま使うのはやめて、今まで使用していたファン付きケースに入れました。こちらのケースです。

製品には小さなヒートシンクもついていますが、Raspberry Piの冷却には、ヒートシンクを使うよりも風を当てるのが効果的だと思います。このファンは5V用なのですが、前のRaspberry Piでは静音を狙って3.3Vで動かしてました。それも故障の原因だったかもしれません。今回は定格の5Vで動かすことにしました。ファンが小さいので、USB扇風機ほどの効果はありませんでしたが、それでも40度ほどで動作するようになりました。

Raspberry Piの電源

以前のRaspberry Pi 4では、Homebridgeのwebページに「電圧が低下した」という警告が出ることがありました。今回も、出力電流が2.1AのUSB電源アダプタに接続していたら、同様の警告が出ました。初期のUSB電源電力の規格は小さく、どのUSB電源でもRaspberry Piで使えるというわけではないようです。調べてみたところ、USB 2.0の電源電流規格は0.5A、USB 3.0, 3.1 Type-Aでも0.9Aらしいです。Type Cになっても、登場当時の5Vの電流は1.5Aで、後の規格で3Aになったようです。

Raspberry Pi 4には、5V 3Aくらいの電源が必要と巷では言われてます。(実測報告では1Aも必要なかったらしいですが)なので、電流容量の大きいUSB ACアダプタを使う必要があると思われます。手元のUSB電源アダプタを探ったら、Qualcomm quick charge 3.0対応のType-Aソケット式ACアダプタが見つかりました。これと同じものです。

quick chargeという規格は、Type-C PD (Power Delivery)と同様に、機器とネゴシエーションして電圧を上げて供給電力を増やす仕組みです。一見するとただのType-Aコネクタですが、対応デバイスには20Vまでの高電圧を出します。一方で非対応デバイスには5Vだけを供給します。ただquick charge 2.0以降ならば、5Vであっても、Type-C並に3A流せることになってます。

Raspberry Pi専用として売られている電源を買わなくても、入手が容易な、Type-C PD対応のUSB電源か、もしくはquick charge 2.0以降のUSB電源を用意すれば、必要とされる5V 3Aの電力を得られます。

まとめ

壊れてしまったRaspberry Pi 4の交換をしました。問題なく動作しています。Intel NUCを使った時の懸案事項だった、Zigbee2MQTTが10分おきにオフラインになってしまう問題も、発生しなくなりました。Raspberry Piは、もともとは教育現場で子供達が短時間使うために作られたコンピュータです。常時稼働させるためには、環境を整える必要があります。それで、記憶媒体はSDカードより堅牢と思われるUSBメモリにして、冷却ファン付きケースに入れて、電源も強化しました。

電気錠のJEM-A端子にESP32を接続してHomebridge / HomeKitで使う

玄関などの電気錠コントローラには、JEM-A端子がついていることが多いです。JEM-A端子は2値の入力と出力の端子を持つ日本電気工業会の標準規格です。この端子にESP32を接続して、MQTTブローカー経由でHomebridgeとインタフェースして、Apple HomeKitのLock Mechanismアクセサリとして動作させました。インタフェースさえ確立すれば、あとはAppleのアプリが適切に制御してくれます。

JEMA端子

エアコンについていたJEM-A端子を使う話をこちらに書きましので参考にしてください。

diysmarthome.hatenablog.com

以下おさらいです。JEM-A端子は、日本電機工業会規格(JEM)で定められた機器のオンオフの制御とその状態モニタを行う端子です。電気錠のほかに、エアコン、電動シャッター、床暖房、照明器具などに付いてます。4ピンの端子で、2本が制御信号、2本がモニタ信号です。おそらくは制御信号端子のところに押しボタンスイッチをつけて、モニター信号端子のところにLEDをつけて、人がスイッチを押すたびに、On/Offするようなアナログな仕組みを前提にしているのではないかと思います。これをコンピュータ回路と接続する場合には、フォトカプラなどで絶縁して使うのが一般的らしいです。

下の回路図は、ネットで見つけたパナソニックのJEMAアダプタの回路図です。電気錠のJEM-A端子に、自社のインターフォンを接続するためのアダプタのようです。右側が他社製品のJEM-A端子で、左側がパナソニック製品の機器側です。JEM-A端子側には100Ωの保護抵抗を直列に入れてフォトカプラが接続されています。

panasonic.bizから引用)

電気錠のJEM-A端子

改造対象の電気錠にもJEM-A端子がついています。電気錠とコントローラは、10年くらい前のMIWAの製品です。電気錠制御盤の端に、JEM-A端子というラベルがあります。色々調べてみると、日本製の電気錠にJEM-A端子がついているのはお約束のようでした。インターフォンと連携するために用意されているようです。「ちょっと待ってください、今開けますから」と言って、インターフォンのボタンを押すと電気錠が解錠されるようなユーザシナリオ実現のために、Panasonicのようなインターフォンメーカーと、MIWAのような錠前メーカーが連携するためにある端子のようです。

JEM-A端子に使われるコネクタはXHコネクタと呼ばれるものです。これの4ピンです。

https://www.jst-mfg.com/product/pdf/jpn/XH.pdf

秋月とか千石とか共立とかマルツとかで1個当たり10円程度で売ってます。ピンや圧着工具(手元になかったのでとりあえずはんだ付けしました)なども必要です。圧着工具は、半田付けすれば無しでもなんとかなりますが、コネクタを外す工具は、絶対に持っていた方が良いです。ペンチやマイナスドライバーで外そうとすると苦労しますし、コネクタやケーブルを損傷させる可能性があります。

今回の電気錠は、オートロックのモードに設定されていました。解錠をしてもしばらく経過すると自動的に施錠されます。その時間を5秒から60秒にDIPスイッチで設定できます。モニター端子は、施錠の時にon (HIGH), 解錠の時にoff (LOW)になるようです。なので、もしコンピュータから解錠するとしたら、その手順は、

  1. コンピュータからJEM-A制御信号に250msの信号を送る
  2. 解錠される
  3. JEM-Aモニター信号が指定の時間(5~60s)offになり、その後onに戻る
  4. コンピュータはこの信号をモニターして解錠・施錠状態を表示する
  5. 施錠される

の順番になるのではと考えました。自動的に施錠される(オートロックされる)ので、施錠のために制御信号を送る必要はないと思われます。

一方で、人が手作業でドアを開錠した場合にも、モニター信号は反応すると思います。なのでそのイベントをiPhoneに通知できれば、手動による解錠も知ることができるかと思います。

JEM-A端子の電気特性を調べる

JEM-A端子には、コントロール用にC1, C2端子が、モニター用にM1, M2端子があります。これらにテスターを当てて電圧を調べました。コントロール端子、モニター端子(onの場合)とも、開放時の電圧は4.9Vでした。TTLレベルの回路のようです。使いやすそうで良かったです。また、それぞれに1kΩの付加抵抗を接続した時、C1-C2端子は0.8V、M1-M2端子は3.2Vでした。この結果、内部は以下のようになっていると予想しました。エアコンのJEM-A端子と似ているけど微妙に違います。規格PDFが有料かつ公開不可なので謎ですが、割と緩い規格なのかもしれません。

C1-C2には単に押しボタンスイッチを直結しても大丈夫のようです。フォトカプラも直結で大丈夫だと思われます。M1, M2の電圧は、施錠状態の時に5V、解錠状態の時に1.4Vになるようでした。500Ωの抵抗が入っている様子なので、M1-M2にもLED直結で問題ないと思います。フォトカプラに直結しても、中のLEDが明るめに点灯しそうですが、大丈夫と思われます。

ESP32に接続する

エアコンの端子とあまり変わりはなかったので、前回エアコンのJEM-A端子に接続した時と同じ回路で、ESP32を電気錠に接続することにしました。エアコンよりもM1側の内部抵抗が低いので、R1は大きくしても良かったのですが、現在の値でもTLP621の推奨動作条件順電流(16mA)の40%くらいと想定されるので、このままにしておきました。R2の100Ωは、単なる気休めで、なくても良いと思います。R3も、この値だと10mAくらいになり推奨順電流の60%くらいです。省エネということで。

まずは前回同様ブレッドボードに作りました(写真は前回の使い回しです)。ユニバーサル基板に実装出来たら追記します。

MQTTThingを設定する

ハードウェアは出来上がったので、今度はHomebridge側を構築します。Homebridgeで鍵 (Lock Mechanism) アクセサリを作れるプラグインはたくさんあります。今回はMQTTThingを使いました。MQTTを使えば動作の確認やデバッグが簡単ですし、将来、Apple HomeKit以外のシステム(例えばHome Assistant)で使うことになっても対応できます。

MQTTについてはこちらをご覧ください。

 

diysmarthome.hatenablog.com

 

HomebridgeにMQTTThingプラグインをインストールして、以下のように設定しました。MQTTブローカーがlocalhostで動いていて、ユーザもパスワードも設定なしの場合です。高いセキュリティが要求されるアクセサリなので、後でパスワード設定しておきます。

電気錠の名前はFront Doorとしましたが、これはホームアプリで上書き変更できます。「玄関」などにすればSiriにお願いするときも日本語発音で伝わりやすいかもしれません。ちなみに英語で名前をつけた場合も、「フロントドア」とカタカナ発音でSiriに伝わります。

{
    "accessory": "mqttthing",
    "type": "lockMechanism",
    "name": "Front Door",
    "topics":
    {
        "getLockCurrentState": "mqttthing/lock/getCS",
        "getLockTargetState": "mqttthing/lock/getTS",
        "setLockTargetState": "mqttthing/lock/setTS"
    }
}

使用するTopicsは、

  • mqttthing/lock/getCS
  • mqttthing/lock/getTS
  • mqttthing/lock/setTS

としました。プラグイン名のmqttthingで始まるように設定しました。その先の名前も、サブクルライブする時にまとめてチェックしやすいように階層にしました。CS, TSは、Current State, Target Stateの略です。

使用するメッセージは、デフォルトのままなので指定していませんが、

  • U
  • S
  • J
  • ?

の文字です。それぞれ "Unsecured", "Secured", "Jammed", "Unknown"の意味です。このメッセージもコンフィグファイルで変更できますが、今回はデフォルトのままで使いました。Jと?は使用しないです。

なお、トピックス名のgetとset、targetとcurrentが、HomeKitの中でどのように使い分けられているかについて詳しい説明は以下をご覧ください。

diysmarthome.hatenablog.com

施錠開錠のエミュレーション

Lock Mechanismがどのように動作するのを知る目的で、iPhoneMacのホームアプリから操作をして、動作と流れるMQTTメッセージを確認しました。人がターミナルの上でMQTTのメッセージを見て、必要ならばメッセージをmosquitto_pubコマンドを使ってパブリッシュします。ESP32が行うべき処理のエミュレーションになります。これを元にESP32をプログラミングすれば良いわけです。

ホームアプリから解錠施錠する場合

先の手順でHomebridgeで設定して再起動するだけで、iPhoneMacのホームに、電気錠アクセサリが自動的に現れます。

この先、どういうメッセージが流されているのか確認するために、MQTTをサブスクライブしておきます。

$ mosquitto_sub -h localhost -t mqttthing/lock/# -v

上記の「施錠済み」の状態で鍵アイコンをクリックすると、

「開錠中...」になります。MQTTには、以下のメッセージが流れました。

mqttthing/lock/setTS U

set Target State, つまりこの値(ここでは開錠)を目標に設定してほしいという依頼が流れたようです。このメッセージを受け取ったESP32は、電気錠を解錠すれば良いわけです。JEM-Aの制御端子を250msの間onにします。そしてJEM-Aのモニター端子をチェックして、これがonになったら、開錠が達成できたことになります。そこで、開錠できた事をget Current Stateで知らせます。

$ mosquitto_pub -h localhost -t mqttthing/lock/getCS -m U

すると表示が「施錠済み」に戻ります。

ここで再び鍵アイコンをクリックすると、set Target StateトピックスにSが流れます。表示は「施錠中...」になります。

そこで、get Current StateとしてSを流せば、

$ mosquitto_pub -h localhost -t mqttthing/lock/getCS -m S

すると表示が「施錠済み」に戻ります。

ここまでのMQTTメッセージの流れをまとめておくと以下になります。iPhoneのボタンを押して、解錠を試みて、解錠されたところで、施錠を試みて、施錠されるという順番です。

  1. mqttthing/lock/setTS U(鍵アイコンをクリック:解錠中...になる)
  2. mqttthing/lock/getCS U(解錠済みになる、他デバイスに通知)
  3. mqttthing/lock/setTS S(鍵アイコンをクリック:施錠中...になる)
  4. mqttthing/lock/getCS S(施錠済みになる、他デバイスに通知)

ESP32でのプログラムでは、setTSトピックスに開錠・施錠の設定指示が流れてきたら対応して、設定が終わったらgetCSで回答するという手順を作れば良いわけです。

他のiPhoneから解錠施錠される場合

HomeKitは、必要に応じてアクセサリの状態を通知してくれます。Lock Mechanismアクセサリの場合、自分の端末で操作していないのに、鍵の状態(Current Status)が変更されると、通知が来ます。例えばiPhoneで鍵を操作するとMacに通知が来て、Macで操作するとiPhoneに通知がきます。不正に鍵を開けられる場合に通知が来るので安全です。

Get Target Stateの使い方

ここまでの説明で出番のなかった、Get Target Stateはどういう場合に使うのでしょうか?getなのでHAPの外の世界の事象で発生するメッセージです。なので、例えば壁に電気錠を開錠・施錠する物理的なマニュアルスイッチが設置されていて、これが押された場合に、get Target Stateを使います。すると、iPhoneなどの表示が、開錠中...または施錠中...の表示になります。マニュアルスイッチで操作された後、施錠の状態が変化するはずですが、その結果をget Current Stateで伝えておくと、ホームの表示が更新されるとともに、通知が表示されます。

   

このように、get Target Stateとget Current Stateはこの順番で、セットで流す必要があります。逆の順番で流すと表示が混乱します。例えば、施錠済みの状態で、

get Current State でUを流すと、ボタンは解錠状態の白になるものの、アイコンは施錠状態、文字表示は「施錠中...」という表示になってしまいます。Targetが示されずにいきなりCurrentが変更されて混乱している様子です。これを治すにはget Target StateにUを流します。

一方、get Target StateにUを流し、get Current StateにUを流すという順番でメッセージを送れば、解錠中...の状態を経て、正しく解錠済みの表示になります。

   

ということで、外部マニュアルスイッチによって操作された場合は、get Target Stateとget Current Stateの両方を、この順番で流しておく必要があります。

ESP32のプログラミング

この手順を、ESP32にプログラムする手順を考えます。以下の処理をすれば良いです。

  • set TSを受け取ったらJEM-Aの制御線にパルスを出す(HomeKitからの依頼)
  • JEM-Aのモニタ線が変化した場合:
    1. これがHomeKitからの依頼で変化した場合はget CSでモニタ線状態を送信し、
    2. これが外部からの操作ならばget TSとget CSを送信する

これを実現するために、HomeKitからの依頼によりJEM-Aにパルスを送ったかどうかを記録しておくためのフラグを一つ用意して、以下のようなプログラムを考えました。

boolean:「MQTTからの要請で設定した」= false;

MTQQのsetTSトピックスにメッセージが来た時の処理
  受け取ったメッセージ(UまたはS)と現状が違ったら     JEM-Aの制御線に250msのパルスを送る     (施錠状態の時は5~60秒間解錠、解錠状態の場合は施錠される)    「MQTTからの要請で設定した」フラグをtrueにする loopの中でJEM-Aモニタ線が変化した時の処理   もし「MQTTからの要請で設定した」フラグ==falseならば     get Target Stateに今の状態(UまたはS)を送る(外部からの操作)   get Target Stateに今の状態(UまたはS)を送る   「MQTTからの要請で設定した」=falseにする

JEM-A端子のM1, M2の値は、LEDを繋いでみた限りでは綺麗に切り替わってました。チャッタリングのような過渡的なノイズはない様子です。でも念のために、JEM-A端子の値が300ミリ秒間変化しない場合に動作するようプログラムしました。実際に完成したプログラムは以下になりました。(ちょっと長いです)

//ESP32 lock mechanism with EspMQTTClient library.
// Aug. 28, 2022. (first version)

#include "EspMQTTClient.h"

EspMQTTClient *client;

//input & output pins and values
#define JEMAOUT 13 //GPIO for photo relay to JEM-A control line.
#define JEMA_ON 1  //value for pulse on
#define JEMA_OFF 0 //value for pulse off
#define JEMAIN 12  //GPIO for photo relay driven by JEM-A monitor line. Pull-up.
#define JEMA_U 1   //value for the lock is Unsecure (Unlocked)
#define JEMA_S 0   //value for the lock is Secure (Locked)

int JEMA_current; //current status of the lock
boolean JEMA_pulsed; //if a pluse has been sent to JEMAOUT

//WiFi
const char SSID[] = "XXXXXXXX"; //WiFi SSID
const char PASS[] = "xxxxxxxx"; //WiFi password
//MQTT
char CLIENTID[] = "ESP32_xx:xx:xx:xx:xx:xx"; //MAC address is set in setup()
//for example, this will be set to "ESP32_84:CC:A8:7A:5F:44"
const char  MQTTADD[] = "192.168.xxx.xxx"; //Broker IP address
const short MQTTPORT = 1883; //Broker port
const char  MQTTUSER[] = "";//Can be omitted if not needed
const char  MQTTPASS[] = "";//Can be omitted if not needed
const char  SUBTOPIC[] = "mqttthing/lock/setTS"; //mqtt topic to subscribe
const char  PUBTARGET[] = "mqttthing/lock/getTS"; //mqtt topic to publish
const char  PUBCURRENT[] = "mqttthing/lock/getCS"; //mqtt topic to publish
const char  PUBDEBUG[] = "mqttthing/lock/debug"; //for debug message

void setup() {
  //Digital I/O
  pinMode(JEMAOUT, OUTPUT);
  pinMode(JEMAIN, INPUT_PULLUP);
  JEMA_current=digitalRead(JEMAIN);
  //Serial
  Serial.begin(115200);
  while (!Serial);
  Serial.println("ESP32 Lock Mechanism started.");
  //MQTT
  String wifiMACString = WiFi.macAddress(); //WiFi MAC address
  wifiMACString.toCharArray(&CLIENTID[6], 18, 0); //"ESP32_xx:xx:xx:xx:xx:xx"
  Serial.print("SSID: ");Serial.println(SSID);
  Serial.print("MQTT broker address: ");Serial.println(MQTTADD);
  Serial.print("MQTT clientID: ");Serial.println(CLIENTID);
  client = new EspMQTTClient(SSID,PASS,MQTTADD,MQTTUSER,MQTTPASS,CLIENTID,MQTTPORT); 
}

void onMessageReceived(const String& msg) { // topic = mqttthing/lock/setTS
  //Serial.println(msg);
  client->publish(PUBDEBUG, "Set TS received.");
  boolean newValue, changed=false;
  if(msg.compareTo("U")==0) { //target state is Unsecure (unlock)
    if(digitalRead(JEMAIN) == JEMA_U) return; //already Unsecured (unlocked). do nothing.
  }
  else if(msg.compareTo("S")==0) { //target state is Secure (lock)
    if(digitalRead(JEMAIN) == JEMA_S) return; //already Secured. do nothing.
  }
  //now send a pluse to toggle the electric lock
  digitalWrite(JEMAOUT, JEMA_ON); //activate JEM-A control line for 0.25 sec
  delay(250); 
  digitalWrite(JEMAOUT, JEMA_OFF);
  JEMA_pulsed=true;//flag to show a pulse has sent.
}

int counter, lastvalue;//counter and memory for same-JEMAIN-value check

void onConnectionEstablished() {
  Serial.println("WiFi/MQTT onnection established.");
  client->subscribe(SUBTOPIC, onMessageReceived); //set callback function
  client->publish(PUBDEBUG, "ESP32 Lock Mechanism is ready.");
  JEMA_current=digitalRead(JEMAIN);//update the current state
  JEMA_pulsed=false;//reset the pulse-flag.
  lastvalue=999; //force update at next loop()
}

void loop() {
  client->loop();  
  delay(10); //10ms delay
  
  int newvalue = digitalRead(JEMAIN);//get current JEMA value
  if(newvalue != lastvalue) {
    lastvalue = newvalue;
    counter=0;//set the counter
  }else if(++counter == 30) {//when 300ms passed after last change
    if(JEMA_current == newvalue) return; //no change, do nothing.
    //JEMA monitor status has chenged
    if(newvalue == JEMA_S){
      if(!JEMA_pulsed) client->publish(PUBTARGET,"S");
      client->publish(PUBCURRENT,"S");
    }
    else if(newvalue == JEMA_U){
      if(!JEMA_pulsed) client->publish(PUBTARGET,"U");
      client->publish(PUBCURRENT,"U");
    }
    JEMA_pulsed = false;
    JEMA_current = newvalue;
  }
}

あとはHomeKitにお任せ

以上で、

の接続が完了しました。ここまで設定すれば、iPhoneMacのホームに設定した電気錠が現れます。そして、電気錠としてのあるべき振る舞いは全てHomeKitが面倒を見てくれます。

まずは、世界中のどこからでも、iPhoneMacのホームから解錠ができます。他の端末や手動で解錠された場合は通知が来ます。

次に、家族、同居人、オフィスならば同僚、一時的な滞在者など、Apple IDを持っている人なら解錠できる人として追加できますし、いつでも削除できます。解錠操作は、登録されたApple IDで二段階認証されたiPhoneまたはMacからのみ可能です。パスワードを共有する方式よりも安全です。

さらには、Siriも電気錠を正しく扱ってくれます。電気錠DIYでありがちなエピソードに、外から大声で叫んだら、家の中のスマートスピーカーが反応して解錠してしまうというオチがあります。今回作った電気錠も、「へいSiri、玄関を開けて」というように発声してSiriに開けてもらうことが可能です。「ドアを開けて」など、多少揺らぎのある発話をしても解錠してくれます。ただし音声解錠は、解錠する権限を持つユーザのiPhoneApple Watchに、持ち主の声でお願いした時だけです。HomePodやMacのSiriにお願いしても、すぐには解錠しません。「続きの操作は個人用デバイスで行う必要があります」と音声応答した後、手元のiPhoneに確認の通知がきます。これをタップすることで解錠されます。Lock Mechanismは、照明やエアコンなどの通常のアクセサリよりセキュアな扱いがされています。

まとめ

JEM-A端子が付いているオートロックの電気錠に、フォトカプラ経由でESP32を接続して、MQTTメッセージで解錠できる電気錠にしました。これをHomebridge+MQTTThingプラグインと組み合わせることで、HomeKitのLock Mechanizmアクセサリにしました。これでどこからでも電気錠を解錠できますし、通知も来ます。Siriにお願いして解錠することもできました。

Tuyaの格安WiFi磁気接触センサ(363円)をHomebridge / HomeKitで使う

AliExpressで格安で売っていた磁気接触センサを使ってみました。TuyaのスマホアプリでWiFi設定して、HomebridgeのTuya公式プラグインを使うことで、HomeKitから使用することができました。

追記)ここで紹介したTuyaのクラウドは最近になって無料期間1ヶ月の制限が導入されたようです。もしかしたら下記で紹介したLocalTuyaで使えるかもしれません。しかし結構大変なので、少し高いですがZigbee対応のセンサを買うのが良いと思います。

diysmarthome.hatenablog.com

TuyaのWiFi磁気接触センサ

購入したのはこちらのセンサです。先月買った時は送料込みで363円でした。今は543円に値上がりしてますが、それでも格安だと思います。磁気スイッチと磁石を使ったWiFi接続方式のセンサで、ドアや窓に取り付けて開閉状態を検出します。販売ページには、磁石側の大きさが43 x 10 x 14 mm、本体側の大きさが68 x 23 x 19 mmと書かれてました。実測したところもう少し大きくて、磁石側の大きさが43.9 x 12.3 x 16.6 mm、本体側の大きさは69.9 x 24.3 x 22 mmでした。(取り付けてしまってから計測したので、実測値はあまり正確ではないです)

Zigbeeのセンサは1,000円くらいで売られているのですが、WiFiの方が技術がこなれているのか安いようです。ただ消費電力が大きいため電池が単4x2でした。Zigbeeのものはコイン電池が普通なので、それと比べると本体サイズが大きいです。届いた箱はこんな感じです。

箱の外観

中身はこんな感じ。

製品写真

本体裏蓋はスライドすると開きます。中に単4電池が2本入ります。この価格なので電池は付属していません。

電池ケース

スマホアプリに登録

まずはTuyaのアプリ、Tuya Smartに登録して、WiFiの設定をします。詳細は以前の記事に書いてありますので、ご覧ください。

diysmarthome.hatenablog.com

Tuya Smartにセンサを登録する手順は以下です。

  1. iPhoneを2.4GHz WiFiに接続しておく
  2. Tuya Smartを起動する
  3. Tuya Smartで「デバイスを追加」を選ぶ
  4. Tuya Smartで「ドアセンサー (WiFi) 」を選ぶ
  5. センサのリセット穴に添付のピンを挿して長押ししてリセット

以下はこの時のスマホ画面です。

   

これでデバイスが追加されます。

この後、センサに磁石を近づけたり、離したりすると、2~3秒で通知が来て、表示されるようになりました。この後Tuyaのアプリは使わないので、iPhoneの「設定」から通知をoffにしておきました。

Tuya開発者ページで設定

Home AssistantやHomebridgeなどでTuyaのデバイスを利用する際、そのデバイスZigbeeバイスならば、Zigbee2MQTTを使えます。

でも今回はWiFiバイスなので、Tuyaのクラウド経由で情報を得る方法を用います。そのためにTuya開発者ページで登録して、スマホアプリと連携設定して、IDを得る必要があります。これも以下で行ったのと同じような手順です。

diysmarthome.hatenablog.com

手順は以下です。

  1. Tuyaの開発者ページに行ってアカウントを作る
  2. そこでcloud projectを作る
  3. そのprojectにスマホのTuya Smartアプリを登録する
  4. これで得られるIDなどをHomebridgeプラグインに設定する

今回は、1から3の手順は終了しているので、4番目のステップ、Homebridgeの設定だけを行いました。

Tuya公式プラグインを入れる

次は、Homebridgeの設定です。前回はIRリモコンに特化したプラグインを入れましたが、今回はセンサーなので、Tuyaの公式プラグインを試してみようと思います。Homebridge Tuya Platformという名前のプラグインです。

github.com

tuyaというアカウントが提供している公式プラグインです。このプラグインに必要な情報は以下の4種類です。

  1. "username": "xxxx@xxxx.com",
  2. "password": "XXXXXX",
  3. "accessId": "xxxxxxxxxxxxxxxx",
  4. "accessKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",

usernameとpaasswordは、スマホアプリで使ったものです。捨てメールアドレスを使ったので、そのアドレスと、その時に設定したパスワードを使用します。

accessIdとaccessKeyは、Tuya開発者サイトで作ったプロジェクトのAccess ID/Client IDと、Access Secret/Client Secretの文字列です。

プラグインGUI設定画面では、このほかに、Project Typeを設定します。これはPaaSにしておきます。またPaaS Platformの指定もあります。今回はTuya Smartというスマホアプリを使っているので、それを指定します。いずれもポップアップメニューから選択するだけなので簡単です。なお、Tuyaからは、Smart Lifeというアプリも出てます。こちらを使った場合はそれを指定します。その結果、コンフィグファイルの設定で確認すると以下のようになりました。

 {
    "options": {
        "username": "xxxx@xxxx.com",
        "password": "XXXXXX",
        "accessId": "xxxxxxxxxxxxxxxx",
        "accessKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "lang": "en",
        "projectType": "2",
        "appSchema": "tuyaSmart",
        "countryCode": 81,
        "debug": false
    },
    "name": "TuyaPlatform",
    "platform": "TuyaPlatform"
},

これでHomebridgeを再起動すると、センサがHomeKitに現れます。

これを開くと、オートメーションを追加したり、通知を設定することができます。

とりあえず、センサーが開放(磁石が離れる)とLED電球が点灯し、センサが閉鎖する(磁石が近づく)と消灯するオートメーションを設定して、テストしました。防犯システムとして使うなら、セキュリティシステムのアクセサリを併用すると良いです。

diysmarthome.hatenablog.com

取り付け具合

アルミサッシに取り付けました。2枚の引き戸の重なる部分に、良い具合におさまりました。この場所ならば、どのように扉が開けられても反応します。Zigbee製品に比べて本体が大きめですが、問題なく取り付けられました。本体底部の単4電池部分が、アルミサッシの金属枠側に来るので、アンテナ位置がサッシ枠から離れ、電波の飛びも問題なさそうです。

応答速度

ただし、反応が遅いのが気になりました。磁石が近づく、または離れる動作をした後、Homebridgeに反映されるまで30秒ほどかかります。スマホのTuya Smartアプリにはほぼ瞬時に通知が来ますが、Tuya開発者サイトのプロジェクトからの通知は、30秒弱(だいたい25秒)経ってから届くようです。

Tuya Smartアプリの動作ももう少し調べてみました。iPhoneWiFi接続にして、このセンサと同じLANにある状態では、ほぼ瞬時、おそらく2~3秒で、アプリが更新されます。ところが、iPhoneWiFiをoffにして、キャリア接続にしたところ、更新されるまでにやはり20秒以上かかりました。おそらくはLAN側から来た問い合わせにはすぐに返答する一方で、クラウドへの報告は20秒間隔くらいで実施しているのではないかと思います。そのぶん省電力になっているとは思うので、応答速度と電池寿命のトレードオフでこの設定になったのかと思います。

防犯センサーのような用途なら問題のない遅延時間だと思います。検出した結果がすぐに必要な場合や、さらにはこれを分解してワイヤレス押しボタンを作るとか考える場合には、30秒弱の応答時間は辛いかもしれません。

まとめ

格安のTuyaのWiFiドアセンサをHomeKitから使えるようにしました。何よりこのコストパーフォマンスはすごいと思いました。家の全てのドアと窓に取り付けても大した金額にならないのではないでしょうか。ちゃんと稼働して、磁石検出を取りこぼすような不具合はありませんでした。応答時間が30秒弱かかる点が欠点ですが、それが許容されるような用途なら、安価で使いやすいと思います。電池は単4なので寿命が長そうですが、WiFiなのでどうでしょうか。今後長期使用して報告します。

WebページからESP32をOTAアップデート

ESP32のプログラムをWeb経由でOTA更新します。ESP32は本来の仕事と並行して、Webサーバとして常時待機しています。これに対して、パソコンのWebブラウザから更新ファイルをアップロードします。Arduino IDEのサンプルスケッチに手を加えて、本体プログラムをわかりやすくしました。

Web経由のESP32 OTAアップデート

前回の記事ではESP32をArduino IDE経由でOTAアップデートする方法について書きました。

diysmarthome.hatenablog.com

これに対して、EPS32をWebサーバとして起動しておいて、web経由でバイナリをアップデートする方法もあります。2つの方法とも、サンプルスケッチが用意されているので、簡単に試すことができます。サンプルスケッチは、ボードマネージャーでESP32開発ボート度登録すると、メニューの「ファイル」「スケッチ例」「EPS32 Dev Module用のスケッチ例」「ArduinoOTA」の中に、BasicOTAとOTAWebUpdaterという名前で現れます。先の記事ではBasicOTAを使用しましたが、今回はOTAWebUpdaterを使用します。

コンパイルしてESP32が起動した後、MacからESP32のアドレスにWeb接続すると、以下のようなページが現れます。

mDNSが動いているので、DHCPがEPS32に割り当てられた数字のIPアドレスが不明でも、eps32.localというアドレスで接続できます。ここでusername, passwordにどちらもadminを入れます(プログラムで変更可能です)。するとファイル選択してupdateできるページが現れます。アップデート中の進捗も表示されます。

アップロードするバイナリファイルは、Arduino IDEのメニューで、「スケッチ」「コンパイルしたバイナリを出力」を選ぶと生成できます。生成結果のファイルは、.binという拡張子を持ち、ソースコードのあるディレクトリに置かれます。当然ですが、更新する新しいプログラムもOTA対応スケッチである必要があります。そうしないと次回のOTAができなくなります。

ここでは、OTAWeb_.....esp32.binというようなファイルをアップデートしました。

プログラムは長い

このサンプルプログラムは結構長いです。WebページのHTMLの部分も変数としてプログラムに書き込まれているので、その部分も長いです。本来のプログラムがわかりにくくなってしまいます。サンプルプログラムを以下に示します。長いことがわかっていただければ良いので、ざっとスクロールして飛ばしてください。

#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Update.h>

const char* host = "esp32";
const char* ssid = "xxx";
const char* password = "xxxx";

WebServer server(80);

/*
 * Login page
 */

const char* loginIndex =
 "<form name='loginForm'>"
    "<table width='20%' bgcolor='A09F9F' align='center'>"
        "<tr>"
            "<td colspan=2>"
                "<center><font size=4><b>ESP32 Login Page</b></font></center>"
                "<br>"
            "</td>"
            "<br>"
            "<br>"
        "</tr>"
        "<tr>"
             "<td>Username:</td>"
             "<td><input type='text' size=25 name='userid'><br></td>"
        "</tr>"
        "<br>"
        "<br>"
        "<tr>"
            "<td>Password:</td>"
            "<td><input type='Password' size=25 name='pwd'><br></td>"
            "<br>"
            "<br>"
        "</tr>"
        "<tr>"
            "<td><input type='submit' onclick='check(this.form)' value='Login'></td>"
        "</tr>"
    "</table>"
"</form>"
"<script>"
    "function check(form)"
    "{"
    "if(form.userid.value=='admin' && form.pwd.value=='admin')"
    "{"
    "window.open('/serverIndex')"
    "}"
    "else"
    "{"
    " alert('Error Password or Username')/*displays error message*/"
    "}"
    "}"
"</script>";

/*
 * Server Index Page
 */

const char* serverIndex =
"<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js'>"
"<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>"
   "<input type='file' name='update'>"
        "<input type='submit' value='Update'>"
    "</form>"
 "<div id='prg'>progress: 0%</div>"
 "<script>"
  "$('form').submit(function(e){"
  "e.preventDefault();"
  "var form = $('#upload_form')[0];"
  "var data = new FormData(form);"
  " $.ajax({"
  "url: '/update',"
  "type: 'POST',"
  "data: data,"
  "contentType: false,"
  "processData:false,"
  "xhr: function() {"
  "var xhr = new window.XMLHttpRequest();"
  "xhr.upload.addEventListener('progress', function(evt) {"
  "if (evt.lengthComputable) {"
  "var per = evt.loaded / evt.total;"
  "$('#prg').html('progress: ' + Math.round(per*100) + '%');"
  "}"
  "}, false);"
  "return xhr;"
  "},"
  "success:function(d, s) {"
  "console.log('success!')"
 "},"
 "error: function (a, b, c) {"
 "}"
 "});"
 "});"
 "</script>";

/*
 * setup function
 */
void setup(void) {
  Serial.begin(115200);

  // Connect to WiFi network
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  /*use mdns for host name resolution*/
  if (!MDNS.begin(host)) { //http://esp32.local
    Serial.println("Error setting up MDNS responder!");
    while (1) {
      delay(1000);
    }
  }
  Serial.println("mDNS responder started");
  /*return index page which is stored in serverIndex */
  server.on("/", HTTP_GET, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/html", loginIndex);
  });
  server.on("/serverIndex", HTTP_GET, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/html", serverIndex);
  });
  /*handling uploading firmware file */
  server.on("/update", HTTP_POST, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
    ESP.restart();
  }, []() {
    HTTPUpload& upload = server.upload();
    if (upload.status == UPLOAD_FILE_START) {
      Serial.printf("Update: %s\n", upload.filename.c_str());
      if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size
        Update.printError(Serial);
      }
    } else if (upload.status == UPLOAD_FILE_WRITE) {
      /* flashing firmware to ESP*/
      if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
        Update.printError(Serial);
      }
    } else if (upload.status == UPLOAD_FILE_END) {
      if (Update.end(true)) { //true to set the size to the current progress
        Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
      } else {
        Update.printError(Serial);
      }
    }
  });
  server.begin();
}

void loop(void) {
  server.handleClient();
  delay(1);
}

短くしてわかりやすくする

これも前回のテクニックを使ってシンプルにします。OTA.hファイルという名前のヘッダファイルを用意してそちらに書いて、本体ではincludeするという方法です。さらに、FreeRTOSの機能を使って、loop()からのserver.handleClient()呼び出しをしなくて良いように書き換えます。前回の記事で紹介したサイトの方法を参考に作りました。元記事では、ESP32なのかの判定とか、FreeRTOSを使うかどうかの設定、シリアルポートの初期化などもできるようになってますが、それらも本体プログラムから全部省略して、極力簡単にしました。

その結果、メインのプログラムは以下のように簡単になります。これくらいシンプルなら、OTA機能を追加しても良いかなという気になりますよね。

#include "OTA.h"

void setup(void) {
  setupOTA("myWiFiSSID", "myWiFiPassword");
  // Your setup code
}

void loop(void) {
  // Your loop code  
}

OTA.hのファイルは長いのでこちらに置いておきました。

Arduino IDEからESP32をOTAアップデート

追記:こちらの改訂版をご覧ください:

diysmarthome.hatenablog.com


 

(以下は古い記事です)

 

OTA (Over The Air)を使ってESP32のスケッチをアップデートするようにしました。通常はESP32とArduino IDEをUSB/シリアル接続するところを、WiFi経由で接続します。ESP32ボードマネージャに入っているOTAスケッチ例よりも、わかりやすい方法が紹介されていましたので試してみました。

OTA

OTAというと、無線経由でファームウェアを入れ替えるようなニュアンスがありました。でもOTAの元々の意味は、無線経由でデータをやり取りすることらしいのです。今や当たり前ですね。やっぱり、OTAを動かしているソフトウェア自体を、OTAで入れ替えてしまう、アクロバティックな使い方が醍醐味な気がします。

ESP32の開発キットをArduino IDEでプログラムする際には、通常はUSB/シリアル接続します。開発段階ではそれで良いのですが、家のどこかに組み込んでしまった後にプログラムを更新する際に厄介です。またUSB/シリアル変換チップを取り外して、ESP32本体だけを組み込んだ場合は、もはやシリアル接続できなくなってます。そこでWiFi経由のOTAでプログラムを更新する方法が使われます。ESPHomeではOTAが用意されていて、簡単に使えました。でも、Home Assistantを使わないとあまりメリットがないです。一方で、Arduino IDEのESP32ボードマネージャにもOTAのスケッチ例が用意されてました。まずはこれを使ってみました。

ArduinoのOTAスケッチ例

ArduinoにESP32のボードマネージャをインストールすると、メニューに、ファイル/スケッチ例/ESP32Dev Module用のスケッチ例/ArduinoOTAが現れ、この中に、BasicOTAとOTAWebUpdaterのサンプルが用意されています。

  • BasicOTAArduino IDEのシリアルの代わりにWiFiで接続してプログラムをダウンロードする方式です。
  • OTAWebUpdaterはES32をwebサーバにして、そこに接続してバイナリをアップロードする方式です。

スケッチ例を試してみました。どちらもすぐに動きました。ただ、OTAWebUpdaterは、webページを作るためにプログラムが長くて、本筋のプログラムがわかりにくくなりそうです。また、BasicOTAの方も長いです。コメントを外しても以下のような感じです。長いです。シリアル経由で進捗状況を報告するようになっていることもあり、そこそこの量があります。またloop()でOTA機能を呼び出し続けなければいけないところも、厄介な気がしました。本来やりたい仕事と、OTAのために実行する仕事が混在しているので混乱しそうです。

#include <WiFi.h>
#include <ESPmDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>

const char* ssid = "..........";
const char* password = "..........";

void setup() {
  Serial.begin(115200);
  Serial.println("Booting");
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("Connection Failed! Rebooting...");
    delay(5000);
    ESP.restart();
  }

  ArduinoOTA
    .onStart([]() {
      String type;
      if (ArduinoOTA.getCommand() == U_FLASH)
        type = "sketch";
      else // U_SPIFFS
        type = "filesystem";
      Serial.println("Start updating " + type);
    })
    .onEnd([]() {
      Serial.println("\nEnd");
    })
    .onProgress([](unsigned int progress, unsigned int total) {
      Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
    })
    .onError([](ota_error_t error) {
      Serial.printf("Error[%u]: ", error);
      if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
      else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
      else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
      else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
      else if (error == OTA_END_ERROR) Serial.println("End Failed");
    });
  ArduinoOTA.begin();
  Serial.println("Ready");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

void loop() {
  ArduinoOTA.handle();
}

なので、ちょっと面倒かなと思ってました。

見た目が簡単なOTA

YouTubeを見ていたら、このESP32のOTAを簡単に見せる方法が紹介されていました。

youtu.be

基本的には上記のサンプルプログラムと同じことをやっているのですが、

  1. 設定の部分を別のヘッダファイルにして、本体プログラムから隠し
  2. freeRTOSの機能を使って、OTAをloop()からではなく、OSスケジューラから呼ぶ

ように変更されています。ソースコードこちらにあります。この結果、全体が以下のように簡単になってます。

#define ESP32_RTOS 
#include "OTA.h"
void setup() { Serial.begin(115200); Serial.println("Booting"); setupOTA("TemplateSketch", "mySSID", "myPASSWORD"); // Your setup code } void loop() { // Your code here }

デバッグのためにシリアルポートを使ってますが、それ以外を見ると、 ヘッダファイルをインクルードして、setup()に1行追加しているだけです。これならOTAを使ってみようという気になります。そこで試してみました。

OTA対応Lチカ

まずはOTAではない、普通のLチカです。GPIOの14番に取り付けたLEDを1秒ごとに点滅します。

#define LED 14

void setup() {
  pinMode(LED, OUTPUT);
}

void loop() {
  digitalWrite(LED, HIGH);
  delay(1000);
  digitalWrite(LED, LOW);
  delay(1000);
}

これをOTA対応に書き換えます。追加するのは太字の3行だけです。OTA.hは同じディレクトリに入れておく必要があります。#defineと#include以外の本体プログラムは、setup()にsetupOTA()が追加されただけです。

#define ESP32_RTOS
#include "OTA.h"
#define LED 14

void setup() {
  setupOTA("BlinkLED_OTA", "mySSID", "myPASSWORD");
  pinMode(LED, OUTPUT);
}

void loop() {
  digitalWrite(LED, HIGH);
  delay(1000);
  digitalWrite(LED, LOW);
  delay(1000);
}

これをシリアル経由で書き込みます。書き込む前は、Arduino IDEの接続手段にシリアルポートしか見えていませんでした。

でもこのプログラムが動き始めると、setup()の中のsetupOta()で指定した名前、BlinkLED_OTAで始まるネットワークポートが見えるようになってます。これを選択すると、新しいプログラムが書き込めます。OTAのネットワークポートが現れない場合は、ESP32をリセット(電源をoff/on)すると良いようでした。

もとのサンプルではSerialの設定もされていました。Serial.begin(115200);という記述です。シリアルは使わないので、その部分は削除しました。OTA.hには、Serial経由でメッセージを伝える部分が残っているので、削除しておいた方が良いかもしれません。gitの方には、シリアルではなくて、telnet経由でメッセージを送るバージョンも公開されています。ただ、MQTTを使う機会が多いので、メッセージはMQTT経由で流せば良いと思いました。それでtelnet版は試していません。

MQTTプログラムをOTA対応にする

ということで、MQTTを使ったプログラムもOTA対応にしました。OTA化したのは、こちらで作ったプログラムです。MQTTからのコマンドでLEDをon/offするものです。

diysmarthome.hatenablog.com

MQTTもOTAもどちらもWiFiを使うので、初期化の部分などでコンフリクトするのではないかと心配したのですが、問題ありませんでした。OTA.hを同じディレクトリに入れて、3行(以下の太文字の行)を追加するだけでOTA対応になりました。

#include "EspMQTTClient.h"
#define ESP32_RTOS
#include "OTA.h"

EspMQTTClient *client;

const int LED=14; //pin for LED
const char SSID[] = "XXXXXXXX"; //WiFi SSID
const char PASS[] = "xxxxxxxx"; //WiFi password
char CLIENTID[] = "diysmarthome_cv3a2JTs";//適当な文字列

const char  MQTTADD[] = "192.168.xxx.xxx"; //Broker addr
const short MQTTPORT = 1883; //Broker port
const char  MQTTUSER[] = "";//Can be omitted if not needed
const char  MQTTPASS[] = "";//Can be omitted if not needed
const char  SUBTOPIC[] = "mqttthing/seton"; //sub topic
const char  PUBTOPIC[] = "mqttthing/geton"; //pub topic 
const char DBGTOPIC[] = "mqttthing/debug"; //debug topic  

void setup() {
  pinMode(LED, OUTPUT);
  client = new EspMQTTClient(SSID,PASS,MQTTADD,MQTTUSER,MQTTPASS,CLIENTID,MQTTPORT); 
  setupOTA("MQTT_OTA", SSID, PASS);
}

void onMessageReceived(const String& msg) {
  client->publish(DBGTOPIC, "Message received.");
  if(msg.compareTo("true")==0) {
    digitalWrite(LED, HIGH);
    client->publish(PUBTOPIC,"true");
  }
  else if(msg.compareTo("false")==0) {
    client->publish(PUBTOPIC,"false");
    digitalWrite(LED, LOW);
  }
}

void onConnectionEstablished() {
  client->subscribe(SUBTOPIC, onMessageReceived); //set callback 
  client->publish(DBGTOPIC, "Connection established. !!!");
}

void loop() {
  client->loop();
}

もっと複雑なプログラムも大丈夫でした。以下で紹介したJEM-A HA端子をコントロールするプログラムも、3行追加でokでした。

diysmarthome.hatenablog.com

少しだけ改良

少しだけ改良しました。そのためにOTA.hを少しだけ書き直しました。ただ、この改良はしてもしなくてもあまり関係ないことなので、書き直したOTA.hは添付しないでおきます。

WiFi初期化の無駄を省く

すんなり動いたのですが、無駄なことをしている点が気に掛かりました。一つは、前述のように、WiFiを重複して初期化していることです。MQTTもOTAもどちらも同じWiFiを使ってます。それぞれのライブラリの中で、SSIDとパスワードを使って初期化してます。2回初期化しているので、最初の初期化は無駄になってるはずです。結局は、WiFiという名前の同一のクラスのクラスメソッドにアクセスしているので問題なく使用できてます。でも無駄なことをしている点が気に掛かりました。そこで、OTA.hの中で、WiFiに関係する行を全部コメントアウトしました。WiFi.hなどのWiFi関係のヘッダファイルのインクルードもコメントアウトしました。

ということで、WiFiの初期化はMQTTライブラリだけで行い、OTAライブラリでは初期化しないようにできました。SSIDとパスワードを渡す必要がなくなったので、setupOTAの引数からそれらを省きました。

ただこの方式では、MQTTライブラリでのWiFi初期化が完全に終了する前にOTAライブラリを起動してしまうと失敗するようです。なので、setupOTAの呼び出しを、setup()の中ではなく、MQTTの接続が完了した時に呼び出されるコールバック関数である、onConnectionEstablished()の中で行うように、場所を移動しました。

Serial.printの無駄を省く

有線では接続しないので、Serialに流れているデバッグメッセージは無用です。とはいえ、無いと寂しいかと思い、MQTTで流すようにOTA.hを変更しました。Serial.print()でフォーマットしているところはsprintf()を使ってテキスト列にしました。進捗報告の場所は、数字を間引いて、表示します。mosquitto_subしておけば、以下のように表示されます。実のところそれほど有用なメッセージは流れていないので、無駄なところに力を使ってしまった気がします。Serialを使用している部分をコメントアウトするか、単に放置しておいても良かったかもしれません。

mqttthing/debug Progress: 0%
mqttthing/debug Progress: 20%
mqttthing/debug Progress: 40%
mqttthing/debug Progress: 60%
mqttthing/debug Progress: 80%
mqttthing/debug Progress: 100%
mqttthing/debug End
mqttthing/debug OTA Initialized
mqttthing/debug IP address: 192:168:XXX:XXX
MQTTにデバッグメッセージを流すためには、MQTT関係の設定が終了した後で、OTA.hをインクルードする必要が生じました。そこで、インクルードする位置を、変数設定の後ろに移動しました。
以上の結果、以下のような場所に追加の3行(太字の行)が入ります。
#include "EspMQTTClient.h"
#define ESP32_RTOS

EspMQTTClient *client;

const int LED=14; //pin for LED
const char SSID[] = "XXXXXXXX"; //WiFi SSID
const char PASS[] = "xxxxxxxx"; //WiFi password
char CLIENTID[] = "diysmarthome_cv3a2JTs";//適当な文字列

const char  MQTTADD[] = "192.168.xxx.xxx"; //Broker addr
const short MQTTPORT = 1883; //Broker port
const char  MQTTUSER[] = "";//Can be omitted if not needed
const char  MQTTPASS[] = "";//Can be omitted if not needed
const char  SUBTOPIC[] = "mqttthing/seton"; //sub topic
const char  PUBTOPIC[] = "mqttthing/geton"; //pub topic 
const char DBGTOPIC[] = "mqttthing/debug"; //debug topic  

#include "OTA.h"

void setup() {
  pinMode(LED, OUTPUT);
  client = new EspMQTTClient(SSID,PASS,MQTTADD,MQTTUSER,MQTTPASS,CLIENTID,MQTTPORT); 
}

void onMessageReceived(const String& msg) {
  client->publish(DBGTOPIC, "Message received.");
  if(msg.compareTo("true")==0) {
    digitalWrite(LED, HIGH);
    client->publish(PUBTOPIC,"true");
  }
  else if(msg.compareTo("false")==0) {
    client->publish(PUBTOPIC,"false");
    digitalWrite(LED, LOW);
  }
}

void onConnectionEstablished() {
  client->subscribe(SUBTOPIC, onMessageReceived); //set callback 
  client->publish(DBGTOPIC, "Connection established. !!!");   
  setupOTA("MQTT_OTA");
}

void loop() {
  client->loop();
}

メインプログラムでは微妙な違いですが、OTA.hでは20行くらい手を入れました。

パスワードを設定する

サンプルプログラムだと、OTAのパスワードが省略されているので、WiFiに接続できる人が、Arduino IDEを起動すれば、誰でもコードを書き換えられます。それはちょっと困るので、パスワードを設定しました。OTA.hの中に、以下の行がありますので、

 // No authentication by default
 // ArduinoOTA.setPassword("admin");

コメントを外してパスワードを設定しておきます。すると書き込みの際にパスワードを聞かれるようになります。

まとめ

Arduino OTAの記述を簡単にする秘訣がYoutubeで紹介されていたので、それを使いました。設定を別ファイル(OTA.h)にして、FreeTOSの機能を使ってloop()での記述を避ける方法でした。gitにあるOTA.hで問題なく動きますし、MQTTなどの、同じくWiFiを使うライブラリとのコンフリクトもありませんでした。その部分を書き直したのですが、作業しなくても問題なく動きます。

Tuyaの格安 (530円) WiFi赤外線リモコンをHomeKitで使う

Tuyaの黒くて丸いWiFi接続赤外線リモコンがAliExpressで送料無料530円で売られていたので、つい買ってしまいました。Homebridgeのプラグインを使ったら、Apple HomeKitから使えました。今は値上がりしてしまいましたが、各部屋においても良いくらいの価格かと思います。少し高いですがAmazonでも売ってます。

追記)ここで紹介したTuyaのクラウドは最近になって無料期間1ヶ月の制限が導入されたようです。

diysmarthome.hatenablog.com

Tuya Smartで設定

まずは、iPhoneAndroidのTuya Smartアプリをダウンロードします。いつものようにメールアドレスで登録する方式なので、macOSの「メールを非公開」機能で作った捨てメアドを使いました。IoT時代にこの機能は必須です。

アプリから、デバイスを追加を選び、電気製品・その他にあるスマートリモコンを選びます。そこで、2.4GHzのWiFiSSIDとパスワードを入力します。次に、デバイスをリセットします。

リモコンの裏にリセットスイッチがあるので、長押しします。すると青色LEDが点滅を始めて、メニューを押すとSmart IRという名前のデバイスが登録されました。このデバイスの中に、「+リモートコントロールパネルを追加する」ボタンがあるので、それを押します。

次にリモコンでコントロールする相手を設定します。ここではエアコンを登録したかったので、エアコンを選びます。この先の「ブランドを選択」する場面で、メーカー名(パナソニック)を選んだところ、すぐにコントロールできました。

Homebridgeプラグインを入れる

Tuya Smartアプリでは、Google HomeAmazon Alexaなどの設定ができるようです。でもHomeKitから使いたいので、Homebridgeのプラグインを探しました。「Tuya」という文字で検索すると多数のプラグインが見つかりました。名前にTuyaの文字が入っているプラグインだけでも、26個あります。みんなが頑張って使えるようにしているようです。その中から、今回はHomebridge Tuya IR Platformというプラグインを選びました。Tuyaが作っている公式のプラグインもありましたが、今回は赤外リモコンしか使わないので、わかりやすくて良いかなと思いました。更新頻度も高いようです。

github.com

Tuyaの開発者ページに登録

Tuyaデバイスの機能を使うためには、Tuyaの開発者ページに登録して、使用しているデバイスに関する情報を入手する必要があります。クラウドでコントロールされているIoTデバイスではよくある手順です。デバイス情報を使って、クラウドに制御を依頼すると、手元のデバイスが動作します。

Tuyaで、デバイスの情報を得る手順は以下でした。

  1. Tuyaの開発者ページに行ってアカウントを作る
  2. そこでcloud projectを作る
  3. そのprojectにスマホのTuya Smartアプリを登録する
  4. これで得られる秘密の番号をHomebridgeプラグインに設定する

いつものことですが、ここからちょっとややこしくなります。

1, 2, 3のステップはこちらにも説明があります。Tuyaの公式プラグインの説明にも、同じような手順が書いてあります。そちらには説明ビデオへのリンクもあります。ただ、開発ページのメニューが変更されているようで、ビデオ通りにはいかず、わかりにくかったです。

アカウントを作る

まずは、Tuyaの開発者ページに行って開発者アカウントを作ります。またメアドが必要でしたので、先ほどスマホアプリの登録で使った捨てメアドを使い回しました。

cloud projectを作る

次にcloud projectを作ります。左のメニューから、Cloudを選び、そこで現れるメニューからDevelopmentを選びます。

そして右上のCreate Cloud Projectを選びます。

次にプロジェクトの詳細を入力します。Project Nameは任意です。適当に名前をつけます。Descri[ptionは書かなくてもokです。Industryは適当に選んでおけば良いようです。一番上に出てくるSmart Homeで良いでしょう。Development Methodは、よくわかりませんがビデオのチュートリアルではSmart Homeを選んでいるので、そうしました。

Data Centerの設定は、近所を選んでおけば良いと思います。日本の場合は、Western America Data Centerがデフォルトらしいですので、それが無難かと思います。複数選べますし、ビデオチュートリアルでも2〜3箇所選んでいる例が多いので、Chinaも選択しても良いかもしれないです。

次に、Authorize API Serviceという選択肢が現れます。右にある4個がすでに選ばれていて、さらに左から追加したいものを加えるという指示です。

後から変えられるような気もしましたが、変更方法がよくわかりませんでした。そもそもどれを選んだら良いかわかりません。プラグインのページに貼ってあるTuyaのビデオををみたら、Device Status Notificationを追加せよとあるので、それを追加します。

プロジェクトとスマホアプリを連携

ここまでは、順調でした。しかし、ステップ3の、Tuya Smartアプリのアカウントをリンクする段階で手こずりました。プロジェクトを選択すると、いくつかのタブが現れます。ここで、Link Tuya App Acountを選ぶと、iPhoneのTuya Smartアプリと連携してくれます。

Add App Acountボタンを押すと、QTコードが現れます。このコードは一時的なもので、時間が経つと無効になります。その場合はもう一回、生成してもらいます。どうやらこのコードを、iPhoneのTuya Smartアプリで読ませれば良いようです。

ただその方法がわかりにくかったです。iPhoneのTuya Smartアプリでプロフィールのページを開くと、右上に小さなアイコンがあります。それをタップするとカメラが起動してQRコードが読めるようになります。これでプロジェクトとスマホアプリのアカウントが連携するはずです。

しかし、実際には「操作が許可されない」というメッセージが出て先に進みませんでした。いろいろ注意事項を読むと、「中国本土の人は、国の規制があってクラウド開発に特別な許可が必要」というような内容が書いてありました。なんで中国の規制に引っかかったのだろうと思い、My AccountのBasic Informationを確認したら、Country/Regionの設定がChinaになってました。設定した覚えがなかったのですが、アカウント登録のデフォルトがChinaで見落としていたのかもしれません。日本に訂正しました。念のために、データセンタからもChinaを除外しましたが、後から試したらデータセンタは関係ないようでした。

この後、パーミッションの設定ページが出ます。デフォルトでは自動で、Readのみだったので、自動でRead, Write, Manage全部okにしておきました。

秘密の番号を手に入れる

これで開発者サイトへの登録が完了し、クラウド経由からデバイスを制御するために必要なIDが得られるようになります。まずは、プロジェクトを登録した段階で、以下の2個のIDが手に入ります。プロジェクトのOverviewを見ると書いてあります。

いずれも長ったらしい英数字です。Access Secret/Client Secretの方は、目のマークをクリックすると現れます。これも最初は、表示してもらえませんでした。ユーザ登録をする際に、デフォルトでなぜか国籍がChinaになってしまっていたようです。それで、先の中国国籍者制約で、制限がかかっていたようです。ユーザプロファイルのCountry/RegionをJapanに訂正したところ、シークレット番号を表示してもらえるようになりました。

プロジェクトのAll Devicesタブを開けるとデバイス一覧が現れ、そこに、

  • Device ID

が書かれてます。これも長い謎の英数字です。IRリモコン本体と、登録したエアコンの両方のデバイスが見えましたが、IRリモコンのIDを使うようです。エアコンのIDは自動検出してくれるようです。

Homebridgeに設定する

こうして得られた3つの英数字を、HomebridgeのTuyaのプラグインに設定します。

Device Regionは多分データセンターの場所のことだと思いました。他はデフォルトでokでした。コンフィグファイルの方では以下のように設定されていました。

{
    "name": "TuyaIR",
    "tuyaAPIClientId": "xxxxxxxxxxxxxxxx",
    "tuyaAPISecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "deviceRegion": "us",
    "smartIR": [
        {
            "deviceId": "xxxxxxxxxxxxxxxx",
            "autoFetchRemotesFromServer": true,
            "configuredRemotes": [
                {
                    "model": "Unknown",
                    "brand": "Unknown"
                }
            ]
        }
    ],
    "platform": "TuyaIR"
}

設定を保存して、Homebridgeを再起動します。しばらくすると、iPhoneMacの画面に、自動的にリモコンが現れました。タイミングにもよるとは思いますが、ホーム.appにアクセサリが表示されるまでに時間がかかることがあります。15分くらいは気長に待つのが良いです。

     

まとめ

Tuyaの赤外線リモコンユニットをHomeKitで使えるようにしました。Tuyaの製品は他社に比べてお買い得だと思います。購入時よりも今は値上がりしてしまいましたが、それでも1000円くらいで買えます。各部屋に1個ずつおいても良いくらいです。高価なNature Remoと比較して、機能や性能がそれほど見劣りする様子はありません。ただ、現在温度の設定が出ないので、温度センサを持っていないのかもしれません。スマホアプリの日本語表示もところどころ変ですが、設定時にしか使用しないので問題ありません。また、Homebridgeに登録するための手間は、どちらも同じくらいでした。

今回はTuyaのリモコンを使いましたが、Tuya公式プラグインを使えばTuyaデバイスが全てHomeKitから使えるようになるらしいです。機会があれば他のTuyaデバイスも試してみたいです。

LED蛍光灯に交換してHomeKit対応照明に改造する

古い蛍光灯管を蛍光管型LEDに交換しました。交換に加えて、不要になった安定器と点灯管ソケットも取り外しました。重い安定器が無くなって軽量になり、消費電力が半減したはずです。空っぽになった照明器具内部にはスマートスイッチ器具を内蔵しました。

LEDへの移行

古い天井蛍光灯器具が一つ点灯しなくなりました。別の箇所の蛍光灯も発熱がひどいです。蛍光灯照明機器はすでに製造終了していて、交換用蛍光管もいつまで供給されるかわかりません。LEDに移行すべき時期かと思いました。

ただ、どちらの照明器具も引掛シーリングではなく、天井直付けで配線直結です。このタイプの照明器具は、交換に電気工事士の資格が必要です。なので、直付けタイプの照明器具はAmazonにも出回ってなくて、あってもコスパが悪いです。

また天井の壁紙や仕上げが、照明器具の裏まで施工されていないことが多いです。その場合、同じかもしくは大きめのサイズの照明器具に交換しないと、未仕上げの天井材が見えてしまいます。今の照明器具はLEDにより小型化しているので、サイズの合う器具や、より大きなサイズの器具はまず見つかりません。

ということで諦めていたのですが、蛍光管と同じ形状のLEDが多数販売されているのに気づきました。LED電球と同じく、LED蛍光灯は蛍光灯の形をした本体の中に、交流100Vを整流・変圧してLEDを点灯する回路が組み込まれてます。それで、古い器具はそのまま流用して、蛍光灯をLED蛍光灯に交換することにしました。

蛍光灯の仕組み

白熱電球と違って、蛍光灯の点灯には放電を開始するための手順が必要です。なのでLED電球のように、単純にLED蛍光灯に置き換えるわけにはいきません。不適切にLED蛍光灯に交換して、火災や事故を引き起こす事例もあるようです。なので、LED蛍光灯は使わないで、器具ごとLED照明器具に取り替えることを推奨するwebページが多いです。その辺りをしっかり理解するために、まずは蛍光灯の仕組みを調べました。

上の図は点灯管(グローランプ)方式の蛍光灯器具です。このほかにラピッドスタート式とかインバータ式などがありますが、古い器具では点灯管方式が一般的です。この器具の点灯手順は以下になります。

  1. 点灯管の中にはバイメタルで作った接点があって、常温では開いてます。
  2. 点灯管の内部には、低圧のガスが封じ込まれているので、スイッチが入れられて電圧がかかると接点の間でグロー放電が始まります。
  3. すると放電の熱でバイメタルの接点が曲がり、接点が導通して大量の電流が流れます。
  4. 一方で、放電が止まるので温度が下がり、しばらくするとバイメタルの接点が開き、放電が再開します。これを繰り返すことで、点灯管は、数秒ごとにon/offが繰り返されるスイッチとして機能します。
  5. 蛍光管の両端にはヒーターが組み込まれていて、点灯管が導通すると加熱されます。
  6. するとそこから熱電子が放出されて、放電しやすい環境を管内に作ります。
  7. 放電の熱が冷めて点灯管が開くと、電流が急停止し、その過渡現象で安定器のコイル部分に高電圧が発生し、これも放電しやすい状況を作ります。
  8. その結果、蛍光管の放電が開始します。
  9. 蛍光灯が放電開始すると、そちらに電流が流れやすくなるので、点灯管は放電しなくなります。
  10. 放電状態は不安定で電流が揺らぐのですが、安定器のコイルが変動を抑えて、電流・明るさを一定に保ちます。

このように、蛍光灯は工夫の塊でした。この蛍光灯を、誰でも簡単にLEDに置き換えるために、以下の2つの方法が考えられました。

  1. 点灯管を取り外す
  2. 点灯管を短絡する

この2つの方法には互換性がないので、間違えると危険です。またどちらの方法も、点灯管方式以外の、ラピッドスタート式やインバータ式の蛍光灯には適用できません。そのことが正しく理解されず、不適切に使用される結果、火災などの問題を引き起こしているようです。以下でこの互換性の無い2つの改造方式を説明します。

両側給電方式

上の蛍光灯回路を見てLEDを点灯する方式を考えます。一つは、点灯管を取り外す方法です。これなら誰にでもできます。点灯管を取り外すと、点灯管がon/offすることは無いので、蛍光灯の両端に定常的な100V ACが現れます。安定器が介在しますが、理想的なコイル部品とみなすことができれば、定常的な電圧には影響しません。なので、両端から電源を得るタイプの蛍光灯型LEDを用意すれば点灯できます。

この方式の問題点は、誰かが知らずに点灯管を戻してしまうと、短絡することです。火災や感電事故を引き起こす可能性もあります。

片側給電方式

次は、逆に、点灯管の部分を通電させる方式です。このタイプの蛍光灯型LED製品には、点灯管型ヒューズが付属してます。これを点灯管の代わりに取り付けます。こうすると、以下のように片側の2ピンに100V ACが印加されるので、ここからLEDに給電します。

 

この方式のメリットは、点灯管を元に戻しても、LEDが点滅するかもしれない程度で、特に問題が発生しないことです。ただ、もしも間違えて両側給電型LEDを取り付けてしまうと、短絡してしまいます。点灯管部分をヒューズに置き換えてあるので、大事には至らないかもしれませんが、避けたいところです。

安定器を取り外す

両側給電も片側給電も、点灯管に細工をするだけでは、どちらも安定器を介してLEDに給電することになります。安定器が理想的なコイルならばほとんど影響はありませんが、実際には細い銅線が何回も巻かれたトランスのような部品です。なので電気抵抗があり発熱します。20Wの蛍光灯器具で7Wくらい消費するらしいです。20W蛍光灯をLEDで置き換えると10W位に節電できるのですが、それと比較すると安定器で消費される7Wの電力(LEDに交換することで電流が半分になるとしても3.5W)は無駄に感じます。

実際に古い蛍光灯の安定器は、顔を近づけると熱気を感じるくらい、手で触れないくらい発熱してました。劣化の結果、正常な安定器よりも多めに電力消費して、発熱がひどくなっていたのかもしれません。

なので、安定器をバイパスするように配線し直す、もしくは安定器を取り外してしまうと良いそうです。安定器を取り外すなら、ついでに点灯管のための配線やソケット部品も取り外してしまったら良いと思います。点灯管についているコンデンサも無駄ですし、点灯管を間違えて取り付けられる心配もなくなります。重量のある安定器を取り外せば、照明器具が軽くなるので、取り扱いが楽になり、天井への負担も軽減するのではと思います。メリットしかありません。

なので、以下のように配線して、蛍光灯型LEDを取り付けることにしました。今回使用したのは、以下の両側給電型LEDです。

片側給電型LED用に配線すると、両側給電のLEDを取り付けた時に短絡する危険があります。でも、両側給電を前提にして配線すれば、間違えて片側給電LEDを取り付けられても点灯しないだけで問題が発生しません。なので両端給電方式を選びました。

実際に作業した結果が以下です。改造前・改造後の写真です。直管蛍光灯が2灯付いている照明だったので、安定器も点灯管ソケットも2個ありました。改造後の写真では、両端給電型のLEDを取り付けています。

↑改造前 ↓改造後

点灯管に接続されていた配線は、長いまま残してあります。これも外してしまえばもっとすっきりするのですが、ひょっとして将来、片側給電のLEDに交換したいと思うかもしれないので、その時のために残しました。

丸型蛍光灯のLED化

ここまでは、直管型蛍光灯のお話でした。丸型蛍光灯も以下のような製品を使用すれば、同じようにLED化できます。丸型蛍光灯用LEDの場合は、両端給電方式のみのようで、片側給電の製品は見当たりませんでした。

今回は、引掛シーリングタイプの丸型蛍光灯をLED化しました。引掛シーリングならば、格安のLED照明器具が多数販売されているので、それに交換した方が手間要らずで、むしろ安価です。でも昔の器具はコストを掛けて丁寧に作られているので、残したいと思う製品もあります。例えば、格安LED照明のランプシェードは薄くてペラペラですが、古い製品には厚さ1.5mmくらいの肉厚なアクリル素材が使用されていたりします。また、窓貝などのちょっと高級な材料を使った製品もあります。蛍光灯型LEDを使えば、趣のあるシェードを簡単に引き継げます。

そんな古風な照明器具の蛍光灯ユニットを取り外したのが下の写真です。蛍光管は黒ずんでて、全体に埃をかぶってます。これを分解して、洗える部分は洗剤で洗いました。

中には、安定器2個、点灯管ソケット2個、豆球ソケット、紐スイッチがぎっしり詰まってました。

これを全部撤去しました。するとガラガラに空いてしまいます。

ついでにスマート化する

もったいないくらいに空間ができてしまうので、ブログの趣旨に合わせてHomeKit対応のスマート照明に改造することにします。空いた空間に、HomeKit対応のWiFiスイッチユニットを組み込みました。これでHomeKitからon/offできます。

HomeKit対応のWiFiスイッチユニットは、以前の記事で紹介したものです。

diysmarthome.hatenablog.com

ちなみに、上の写真で蛍光灯型LEDに接続しているコネクタも、LEDに付属していた新品コネクタです。なので、引掛シーリングケーブルより器具側の電気系統は、全部新品になりました。軽量になり、消費電力も半分以下に削減できたはずです。

HomeKitからコントロールする

先の記事で紹介したように、このWiFiスイッチはHomeKit対応WiFiアクセサリとして機能します。なのでホーム.appからペアリングすると、iPhoneなどからon/offできるようになりました。この照明器具は、壁スイッチの無い古い部屋にあり、紐スイッチでon/offされていました。スマート化された照明器具が、壁スイッチでoffにされてしまうというよくある問題は発生しません。

さらに手動でon/offしたい場合に備えて、壁にZigbeeスイッチを取り付けてリモコンにしました。このスイッチも、先の記事で紹介したものです。

diysmarthome.hatenablog.com

まとめ

古い照明器具の蛍光灯を、蛍光灯型LEDに交換しました。その際に不要になった安定器や点灯管を除去すると、さらに省エネになり軽量化されます。余ったスペースには、HomeKit対応のWiFiスイッチを内蔵してスマート照明に作り替えました。

TuyaのZigbee壁スイッチ

スマホやスマートスピーカでon/offできるようになっても、手っ取り早く操作できる昔ながらの壁スイッチが欲しいことがあります。IKEAのスイッチに続き、別の壁取り付けZigbeeスイッチを試してみました。Tuyaのヨーロッパ風壁スイッチです。壁には、両面テープ、またはネジで固定します。薄いので、通常の壁スイッチのように見えます。送料込みで1,491円でした。1連、2連、3連がありますが、どれも同じ値段です。

開けてみる

箱の写真は3連ですが、買ったのは1連です。箱は共通のようです。

1連は、のっぺりとしてあまりスイッチらしく見えないです。

ケースは、噛み合わせで固定されています。かなり硬いですが、電池交換やねじ止めする際には、開ける必要があります。中には、スイッチと周辺回路が3個分用意されてました。1,2,3連で同じ内部基板を使っているだろうとは思いましたが、部品まで載ってました。なので価格も同じということのようです。もっとも2個のスイッチは押しても反応しません。プログラムが違うのか、ジャンパー線・抵抗で構成を設定してあるのかと思います。

こちらは裏側です。

使われている本体チップには、機種名の記載がなくて不明です。QRコードの書かれたシールを剥がしてみましたが、その下にも何も書かれていません。形状やピンの様子から、Tuyaが提供しているZigbeeモジュールのようです。

solution.tuya.com

Zigbee2MQTTで使用可能

Zigbee2MQTTで使用可能なのかどうか不明でしたが、可能でした。電池を取り付けて、長押しして、ペアリングモードにしたところ認識されて、Homebridge, HomeKitにスイッチとして表示されました。

バッテリ残量低下の表示が出ますが、実際には新品バッテリを入れてあります。動作には支障ありません。設定を見ると、TuyaのTS0041という製品として認識されているようです。

スイッチ動作は3種類

スイッチアクションには、1回押し、2回押し、長押し(シングルクリック、タブルクリック、ロングプレス)の3種類があります。それぞれに動作をプログラムできます。照明のon/offに対応させるならば、シングルクリックでon、ダブルクリックでoffと設定するのが良いです。

長押しは、ペアリングと紛らわしいので、使用しない方が良いです。マニュアルには、5秒押し続けると長押しになり、10秒押し続けるとでペアリングになると書かれてます。しかし、実際には、約3秒で長押し、約5秒でペアリングになりました。いずれにしても、不用意に長押しし過ぎると、ペアリングモードに入ってしまいます。ペアリングのやり直しになると、新しいスイッチが登録されたことになり、せっかく設定した動作が消えてしまいます。ペアリングに入る条件をもっと稀な操作に設定するか、IKEAのTRÅDFRIスイッチのように、ペアリング専用のスイッチを用意して欲しかったと思います。

長押し動作が洗練されていないことも、誤ったペアリング動作を誘発しています。例えばIKEAのTRÅDFRIスイッチは、長押ししたまま規定の時間が経過すると、長押しイベントが発生します。なので、長押ししている間に期待した動作が確認できた時点で、指を離せば良いわけです。それに対してTuYaのスイッチは、指を離した段階で、ようやく長押しイベントが発生します。指を離すまで、長押しに成功したかどうかわかりません。そのために、長く押しすぎて、ペアリングモードに入りやすくなってます。

まとめ

TuyaのZigbee壁スイッチを使ってみました。Zigbee2MQTTですぐに動きました。ただ、IKEAのTRÅDFRIスイッチの方が、

  • 技適マークがある
  • 価格が安い、または同程度(on/offスイッチは799円、プッシュスイッチは1,499円)
  • 通販の納期が早い、IKEAの店頭に行けばすぐに買える
  • 電池残量が正しく表示される
  • 長押し動作の使い勝手が良い
  • 間違ってペアリングモードに入る心配が無い

というメリットがあり、IKEA製品の方が良いと思います。でも、通常のヨーロッパ型壁スイッチにそっくりな外観は魅力です。IKEAのスイッチは、磁石固定方式なので、外して好きな場所から使用できます。その一方で、固定された普通の壁スイッチを期待する初見ユーザにはわかりにくいかもしれません。

HomeKitでスマホ通知機能付きセキュリティシステムを作る

人感センサ、扉開閉センサなどのHomeKitアクセサリに連動するセキュリティシステムをHomebridge / HomeKitで作りました。HomeKitで定義されているSecurity Systemsカテゴリのアクセサリとして動きます。家を空けてどこに出かけていてもiPhoneに通知が届きます。

センサとブザーを繋ぐ

人感センサや開閉センサをブザーに接続すれば防犯装置が作れます。例えば100Vで鳴動する電磁ベルやブザーを、スマートプラグに接続して、センサをトリガーにonすれば簡単な警報装置が作れます。HomeKitならば、「新規オートメーション」から、「センサが何かを検知したとき」を選んで、スマートプラグをonにすれば良いです。

でも実用性を考えると、いろいろ欲しい機能が出てきます。警報が鳴ったら警備員さんが来てくれたら嬉しいですが、そこまで求めなくても技術的になんとかなりそうな機能は多いです。例えば、外出中に通知が欲しい、簡単に警戒状態のon/offをしたい、その状態をわかりやすく把握したいなど、作り込みたいところです。

セキュリティシステム

HomeKitアクセサリのカテゴリにセキュリティシステムというのがあります。これを使えば、セキュリティシステムに欲しい機能が実現できるのではと考えました。Homebridgeのmqttthingプラグインはセキュリティシステムアクセサリをサポートしているので、これを入れて、MQTTブローカに色々メッセージを送って、アクセサリの機能を調べてみました。

その結果、HomeKitのセキュリティシステムとは、以下のような製品を想定しているのだと理解できました。(写真をクリックするとAliExpressのページに飛びます。)

この製品は、壁に取り付けるコントローラに、無線接続の人感センサ6個、ドア開閉磁石センサ8個、警報ブザー、リモコンスイッチ2個が付属しています。コントローラの操作パネル、またはリモコンから、セキュリティモードに設定すると、異常があった時にブザーを鳴らしてくれます。さらにWiFiGSM経由で異常を知らせてくれるようです。この構成で16,000円なのは安いと思いました。HomeKitのセキュリティシステムアクセサリは、HAPから警戒モードのon/off/切り替えが可能なコントローラを想定しているようです。

自作セキュリティシステム構想

この商品は、Tuya社のシステムに準拠しているようです。Tuyaのデバイス類をHomebridgeから扱えるようにするプラグインが、Tuya社から提供されてます。それを使えば、もしかしたこれもHomeKitから使用できるのかもしれません。でもTuyaのシステムは(他のIoT製品もたいていそうですが)インターネット上のクラウドサービスを前提にしています。Home Assistantの人たちもが言っているように、クラウドにはできる限り頼りたくないです。

そこで、これと同じようなセキュリティシステムを、下の写真のような構成で自作することにしました。センサからの信号を受けて、警戒モードに従って警報器を鳴らす本体コントローラをESP32で作ります。警報器(ブザー)は、ESP32のGPIOでon/offします。コントローラには、テンキーやディスプレイは取り付けず、人とのインタフェースは、HomeKitに任せます。またセンサ類も、Homebridge, HomeKitのシステムを使います。リモコンの代わりがiPhoneです。もちろん無線の壁取り付けスイッチなどをセキュリティモードのon/offに使っても良いです。HomeKitからiPhoneに通知できるので、遠隔地からのモニター機能もこれで実現できます。

警報器(アラーム)を決める

大きな音の出るブザーやベルを探します。電圧は、100Vでも12/24Vでも良いでしょう。見た目とか音の良さから、古風な電鈴(電磁ベル)も良いかもしれません。

AliExpressだと少し安いですが、AC 100/110V用のものはあまり無く240V ACが多いです。一般的にベルよりはブザーの方が安価です。AliExpressで数百円くらいのものが売られていたので、とりあえず買ってみました。LEDもついていて、点滅します。防犯用というよりは、制御室や工場にありそうな警告灯のような製品かもしれないです。DC12V用の製品を買いました。経験的に12Vくらいまでならそれほど感電する心配はありません。でも24Vになるとけっこうきます。

ESP32の回路を考える

今回のESP32のハードウェア的な仕事は、警報器として使うDC 12Vブザーをon/offすることです。DC 12Vのon/offならばパワーFETでできそうです。以下の回路を考えました。

2SK4017には保護ダイオードが入っているようなので、出力側の心配は不要と考えました。耐圧は60Vくらいあります。今回は12Vですが、24Vのブザーでもokのはずです。ゲートからGNDに入れた10kΩは、offになった時のゲートの電荷を逃すために必要とのことで入れました。GPIO13とゲートの間に入れた100ΩはFETへの突入電流などを抑える効果があるそうで、数十Ωから数百Ωあると良いらしいです。FETの入力側ゲート電圧は、仕様書によると閾値電圧が最大で2.5Vでした。100Ωと10kΩの分圧ならばESP32の3.3V出力でもいけそうです。実測で3.2Vくらいは印加できてました。

FETにスイッチされる側には、ねじ止めのターミナルを用意しました。この片方にDC12VのACアダプタを接続し、もう片方に12V用のブザーを接続すればon/offできます。さらに、警報状態の表示のために、GPIO12番にLEDも接続しました。ESP32に押しボタンスイッチを取り付ければ、警報状態の切り替えも出来るかもしれませんが、操作することはないと思い止めました。

Security Systemアクセサリの動作確認

先の述べたように、HomeKitにはSecurity Systemsというカテゴリのアクセサリが用意されています。この動作の詳細を調べました。

Security Systemsは、センサを感知して警報を鳴らすタイプのアクセサリです。mqttthingプラグインを入れて、ダミーのアクセサリを作り動作を見てみます。mqttthingを使うと、mosquitto_sub, mosquitto_pubコマンドでアクセサリの振る舞いを確認して、アクセサリを操作できるので、動作確認がわかりやすいです。

mqttthingの設定から、新規のSecurity Systemアクセサリを選び、適当なMQTTトピックス名を設定して、Homebridgeを再起動します。するとiPhoneMacのホームに、ダミーのセキュリティシステムアクセサリが現れました。名前はSecurity Systemにしました。

クリックすると、在宅、不在、夜間、オフの選択肢が現れます。

在宅、不在、夜間、オフは、MQTTのメッセージでは、デフォルトでそれぞれSA, AA, NA, Dの文字列で表現されてます。STAY_ARM, AWAY_ARM, NIGHT_ARM, DISARMEDの略です。さらに、警報が鳴っている状態のT (ALAEM_TRIGGERED)もあります。それぞれが想定する状態は以下です:

  • 在宅 (SA):人がいる場合の警報レベル。例えば、火災報知器、ガス漏れ、普段は開けることのない扉などが反応したら警報を鳴らす。
  • 不在 (AA):家族全員が外出しているなど、人がいない場合の警報レベル。どのセンサが反応した場合でも警報を鳴らす。
  • 夜間 (NA):夜間の警報レベル。例えば、戸締り関連のセンサ類、屋外のセンサ類に反応があれば警報を鳴らすが、屋内の人感センサはオフにする。
  • オフ (D):全てのセンサをオフにする。警報は鳴らない。

これを全部使用する必要はなく、configファイルで選択できます。例えば、AAとDだけを使えば、一番簡単な、警報on/offスイッチになります。

HomeKitからは、セキュリティシステムアクセサリの動作モードの取得と設定が可能です。なので例えば、「警報が鳴るT状態になったら照明を点灯する」とか、「家に誰もいなくなったら警報レベルAAに設定する」などのオートメーションを作ることができます。

また、動作モードを変更するとiPhoneMacに通知が来ます。なので、設定状態をいつも確認することができます。さらには、警報が鳴るT状態になると、「重大な通知」が送られます。

「重大な通知」は通常に比べて特別扱いの通知で、iPhoneがロックされていても、少し大きめの音が鳴り、緊急状態を知らせてくれます。他の通常通知と同様、設定でoffにすることも可能です。

アクセサリの用意

セキュリティシステムを作るにあたり、次の2つのアクセサリをmqttthingを使って作りました。実態はどちらもESP32で、MQTTブローカー経由のメッセージに反応して動作します。

  • Security Systemアクセサリ:セキュリティレベルの取得と切り替えを担当する
  • Alarm Buzzerアクセサリ:警報アラームのon/offをするスイッチ。センサからの通知を受け付ける。

     

人感センサ、開閉センサなどは、検出時にAlarm Buzzerをonにするようオートメーション設定をしておきます。鳴動し続けるのが困るようならば、例えば3分て停止するように設定します。

他にも必要なセンサがあれば、すべてAlarm Buzzerをonにするように設定しておきます。それで、実際に警報ブザーを鳴らすかどうかは、Security Systemの設定によって、ESP32が判断します。

Homebridgeの設定

Homebridgeには、これらの2つのアクセサリをmqttthingで設定します。それぞれの設定内容は以下です。restrictTargetStateで使用する警報レベルを指定しています。ここでは、

1: 不在 (AA)

3: オフ (D)

を使用するよう設定してます。

{
    "type": "securitySystem",
    "name": "Security System",
    "topics": {
        "setTargetState": "mqttthing/security/setTargetState",
        "getTargetState": "mqttthing/security/getTargetState",
        "getCurrentState": "mqttthing/security/getCurrentState"
    },
    "restrictTargetState": [1, 3],
    "accessory": "mqttthing"
},
{
    "type": "switch",
    "name": "Alarm Buzzer",
    "topics": {
        "getOn": "mqttthing/security/getCurrentState",
        "setOn": "mqttthing/security/setTargetState"
    },
    "accessory": "mqttthing"
},

2つのアクセサリは、どちらもESP32が担当します。そこでトピック名を共通にして、後のプログラミングを簡単にしています。

MQTTメッセージとESP32の応答

MQTTブローカを介して交換されるメッセージを図示しておきます。ホーム.appで人がセキュリティシステムのモードをAAまたはDに切り替えると、setTargetStateトピックにAAまたはDのメッセージが流れます。

一方、センサに反応があると、setTargetStateトピックにtrueのメッセージが流れます。反応が止まればfalseが流れます。

ESP32はこのトピックをサブスクライブしておきます。そして、メッセージAAもしくはDを受け取ったら、内部の状態をそれにセットし、getCurrentStateトピックにAAもしくはDメッセージを流します。これでホーム.app上のアイコンの表示が変化します。

また、メッセージtrueを受け取って尚且つ自身の状態がAA(警戒状態)だったら、ブザーを鳴らします。またgetCurrentStateにTを流して、ホーム.appにも知らせます。

アイコンの変化とMQTTメッセージの関係を順番に例示します。まず、最初はSecurity SystemがD状態だっとします。

ここで、iPhoneでonにする操作をすると、MQTTのsetTargetStateトピックにAAが流れます。アイコンは「警戒状態に移行中・・・」になります。

これを受け取ったESP32が、内部状態を設定して、getCurrentStateトピックにAAを流すと、アイコンは「不在」に変わります。色も警戒色になります。デバイスからgetCurrentStateが返されて初めて、警戒状態に設定されることで、確実な動作を確認できるよう配慮されてます。

この状態で、getCurrentStateにTが流れると、以下のように作動済みになります。この時、前述のように「重大な通知」が配信されます。

ESP32のプログラム

ESP32にこの動きをプログラムした結果が下記です。

//ESP32 security system (AA Alarm) with EspMQTTClient library.

#include "EspMQTTClient.h"
EspMQTTClient *client;

//input & output pins and values
#define ALARMOUT 13 //GPIO for a relay to activate alarm buzzer.
#define ARMEDLED 12  //GPIO for armed-indicator LED.

//security level. Only AA (Away Arm) and D (Disarm) are used.
#define level_SA 0  //Stay Arm, not used in this version
#define level_AA 1  //Away Arm
#define level_NA 2  //Night Arm, not used in this version
#define level_D 3   //Disarm
int currentLevel=level_D; //current security level 0=SA, 1=AA, 2=NA, 3=D

//WiFi
const char SSID[] = "XXXXXXXX"; //WiFi SSID
const char PASS[] = "xxxxxxxx"; //WiFi password
//MQTT
char CLIENTID[] = "ESP32_xx:xx:xx:xx:xx:xx"; //MAC address is set in setup()
//for example, this will be set to "ESP32_84:CC:A8:7A:5F:44"
const char  MQTTADD[] = "192.168.xxx.xxx"; //Broker IP address
const short MQTTPORT = 1883; //Broker port
const char  MQTTUSER[] = "";//Can be omitted if not needed
const char  MQTTPASS[] = "";//Can be omitted if not needed
const char  SUBTOPIC[] = "mqttthing/security/setTargetState"; //mqtt topic to subscribe
const char  PUBTARGET[] = "mqttthing/security/getTargetState"; //mqtt topic to publish
const char  PUBCURRENT[] = "mqttthing/security/getCurrentState"; //mqtt topic to publish
const char  PUBDEBUG[] = "mqttthing/security/debug"; //for debug message

void setup() {
  //Digital I/O
  pinMode(ALARMOUT, OUTPUT);
  pinMode(ARMEDLED, OUTPUT);
  digitalWrite(ALARMOUT, LOW); //set alarm off
  digitalWrite(ARMEDLED, LOW); //set LED off
  currentLevel=level_D; //set disarm
  //Serial
  Serial.begin(115200);
  while (!Serial);
  Serial.println("ESP32 Security System started.");
  //MQTT
  String wifiMACString = WiFi.macAddress(); //WiFi MAC address
  wifiMACString.toCharArray(&CLIENTID[6], 18, 0); //"ESP32_xx:xx:xx:xx:xx:xx"
  Serial.print("SSID: ");Serial.println(SSID);
  Serial.print("MQTT broker address: ");Serial.println(MQTTADD);
  Serial.print("MQTT clientID: ");Serial.println(CLIENTID);
  client = new EspMQTTClient(SSID,PASS,MQTTADD,MQTTUSER,MQTTPASS,CLIENTID,MQTTPORT); 
}

void onConnectionEstablished() {
  Serial.println("WiFi/MQTT onnection established.");
  client->subscribe(SUBTOPIC, onMessageReceived); //set callback function
  client->publish(PUBDEBUG, "ESP32 Security System is ready.");
}

void onMessageReceived(const String& msg) { // topic = mqttthing/security/setTargetState
  //Serial.println(msg);
  client->publish(PUBDEBUG, "Set TS received.");
  
  if(msg.compareTo("AA")==0) { //is set AA (Away Arm)
    currentLevel = level_AA;
    //inicate with a 0.2 second beep
    digitalWrite(ALARMOUT, HIGH); delay(200); digitalWrite(ALARMOUT, LOW);
    digitalWrite(ARMEDLED, HIGH); //turn LED on
    client->publish(PUBCURRENT,"false"); //turn off the button JIC it is on
    client->publish(PUBCURRENT,"AA");
  }
  else if(msg.compareTo("D")==0){ //target state is D (Disarm)
    currentLevel = level_D;
    digitalWrite(ARMEDLED, LOW); //turn the LED off
    digitalWrite(ALARMOUT, LOW); //turn the alarm off JIC it is on
    client->publish(PUBCURRENT,"false"); //turn the button off JIC it is on
    client->publish(PUBCURRENT,"D");
  }
  else if(msg.compareTo("false")==0) { //a sensor turned off the switch
    if(currentLevel == level_AA){ // if AA, alarm likely to be triggered
      client->publish(PUBCURRENT,"AA"); //return to AA from T 
      digitalWrite(ALARMOUT, LOW); //turn the alarm off
    }
  }
  else if(msg.compareTo("true")==0) { //sensor turned on the switch
    if(currentLevel == level_AA){ //if it is armed
      client->publish(PUBCURRENT,"T"); //trigger the security system
      digitalWrite(ALARMOUT, HIGH); //turn on the alarm buzzer
    }
    else{ //if currentLevel is level_D
      delay(500); //do nothing but just delay for 500 ms.
      client->publish(PUBCURRENT,"false"); //turn off the button on the Home.app
    }
  }
}

void loop() {
  client->loop();
}

セキュリティシステムをAAモードに設定した場合には、ブザーを0.2秒鳴らして知らせています。

またセキュリティモードがDの時に、センサから通知があった場合は、Alarm Buzzerのアイコンを0.5秒だけonにしてそのことを知らせてます。この場合はブザーを鳴らしません。センサの動作確認として便利かと思います。

ブレッドボードで試作する

回路とプログラムの動作確認のために、ブレッドボードで試作しました。12VのACアダプタも接続して、FETの動作も確認しました。(最初の試作でドレインとソースを逆にしていて動かなかったのは内緒です)

ホーム.appの操作、アイコンの表示、センサの動作、MQTTメッセージの流れ、プログラムの動作を、いろいろなシーケンスを試して確認しました。訂正すべき点がたくさんあったので、こういうテストは必須でした。

ユニバーサル基板で工作する

回路とプログラムの動作確認ができたところで、ユニバーサル基板で配線しました。Fritzingのプリント基板設計機能を使うと、部品配置を検討できて便利ですね。

適当なサイズの基板がなかったので、少し大きめです。壁に貼り付けて使うことになるので、大きくても良いかと思いました。

あまり綺麗じゃない裏側です。

これを壁に取り付けます。ブザー用の12V DCとESP32 dev用の5V DCの2個のACアダプタを使うので、テーブルタブレットも取り付けました。

こちらがブザーです。上記のサイトで売られている商品はDC12V用が青色で、DC24V用が赤色でした。これはDC12V用です。

まとめ

HomebridgeとESP32を使って、センサに反応があると警報器が鳴るセキュリティシステムを作りました。設定や通知がHomeKitでサポートされていて、遠隔地から状況を把握できて便利です。しばらく使い続けて使い勝手を確認したいと思います。