前回の記事で部屋の自己発電じこはつでんスイッチの照明を Home Assistant につなぎ、スマホから操作できるようになった。次にやりたいのは、ベッドに寝転んだまま音声で照明をオン・オフすることだ。

やり方は、home-assistant-matter-hub で HA のエンティティを Matter ブリッジとして包み、リビングの Google TV をハブにするというもの。経路はすべて LAN 内で完結し、クラウドを通らない。構築自体は難しくなく、時間がかかったのはペアリングのほうだった。Google Home がずっと「デバイスが見つかりません」と表示し続け、最終的に2つの問題が重なっていたことがわかった。1つは開発中デバイスに対する Google の制限、もう1つは僕自身のサーバーのファイアウォールだ。

以下、順番に整理する。なぜ Matter を選んだのか、matter-hub の設定、ペアリングで通過する関門、そしてそれぞれで詰まったときにどう確認してどう直したか。最後に、ペアリング後に音声操作を実際に使ってみて出てきた問題を記録しておく。

なぜ Matter なのか#

HA を Google Home につなぐ方法はだいたい3つある。

Nabu Casa Cloud手動の google_assistant 連携Matter ブリッジ
費用月額サブスク無料無料
操作経路スマホ → Google クラウド → Nabu Casa → HAスマホ → Google クラウド → 外部公開した自宅の HALAN 内で直結
外部公開不要/api/google_assistant を Google から叩けるようにする不要
ネット断で使えるか使えない使えない使える
前提条件有料Google Cloud プロジェクト、OAuth、サービスアカウント家に Matter ハブが必要

Matter ブリッジには Matter ハブが1台必要になる。最初は家にある SwitchBot Hub 3 で行けると思っていたが、公式の説明を読むと、あれはブリッジでしかなく、別途サードパーティのプラットフォームのホームハブが必要だとわかった。

それで一度は2番目の方法で行こうとした。そのあと、リビングの Google TV Streamer 4K がそもそも Matter ハブに対応していることを思い出し、3番目に戻した。クラウド不要、外部に何のエンドポイントも開けなくていいし、CrowdSec が Google のサーバーを誤ってブロックする心配もない。

matter-hub を立てる#

の元のプロジェクト(t0bst4r)は 2026 年 1 月にメンテナンスを終了してアーカイブされ、最終版は v3.0.4。現在は RiDDiX の fork が引き継いでいる。僕は最初は元のプロジェクトを使っていたが、クラッシュするバグのせいで fork に乗り換えた(後述)。これから構築するなら、最初から fork の image を使うのがおすすめだ。

yaml
services:
  matter-hub:
    image: ghcr.io/riddix/home-assistant-matter-hub:latest
    container_name: matter-hub
    restart: unless-stopped
    network_mode: host                     # 必須。Matter は mDNS と IPv6 で探索する
    environment:
      - HAMH_HOME_ASSISTANT_URL=http://<HAのアドレス>:8123/
      - HAMH_HOME_ASSISTANT_ACCESS_TOKEN=${HA_TOKEN}
      - HAMH_HTTP_PORT=8482                # 管理画面
      - HAMH_MDNS_NETWORK_INTERFACE=enp6s0 # ホストに NIC が複数あるときは、本当の LAN インターフェースに固定する
      - TZ=Asia/Taipei
    volumes:
      - ./data:/data

設定まわりの細かい点。

  • network_mode: host は省略できない。Matter の探索は mDNS(UDP 5353)と IPv6 を使うので、bridge モードでは受け取れない。
  • 使うポートは 5353(mDNS)、5540/5541(Matter)、8482(管理画面)。ドキュメントにも開けるよう書いてある。
  • トークンは HA の長期アクセストークンを使う。左下のアバター → セキュリティ → 一番下の「長期アクセストークン」。僕は .env に置いて、パーミッションを 600 にした。
  • 僕のホストには LAN 以外にも WireGuard、Incus のサブネット、NanoKVM の USB NIC、大量の docker bridge がある。インターフェースを限定しないと、ログに wg0: send Unknown system error -126 がひたすら出る。HAMH_MDNS_NETWORK_INTERFACE を LAN の NIC に設定したら消えた。これはログに影響するだけで、後のペアリング問題とは関係ない。

起動したら :8482 の管理画面で bridge を作る。入れたいのは照明2つだけなので entity_id で絞り込もうとしたが、ドロップダウンにその選択肢がなかった。

pattern に完全なエンティティ ID を入れれば完全一致になる。domain で switch を選ぶとすべてのスイッチが送られ、platform で esphome を選ぶと WiFi 信号や IP といった診断用センサーまで一緒に送られてしまうので、この2つは要注意。

保存するとペアリングコードと QR コードが生成される。Google Home アプリでの操作は + → デバイスのセットアップ → 既存のデバイスをお持ちですか? → Matter 対応デバイス。

ペアリングで通る関門#

まずペアリングの流れを分解しておくと、どこで問題が起きているのか後で照らし合わせやすい。

スマホが mDNS でブリッジを発見 Google がVID / PID をチェック スマホが IPv6 でUDP 5540 に接続 ペアリング完了スマホとハブが接続

厄介やっかいなのは、2番目と3番目の関門のどちらで失敗しても、Google Home には同じ画面が出ることだ。

しかもどちらの場合も matter-hub のログは空っぽで、スマホがブリッジを見つけたのかどうかすらわからない。僕は両方の関門で躓つまずいた。

第1関門:mDNS 探索#

最初に疑ったのはネットワークの分断だった。スマホと Google TV は 5GHz、サーバーは有線。ただ、ペアリングの段階で通信が必要なのはスマホとサーバーの間で、Google TV はペアリング完了後にハブとして引き継ぐだけなので、テレビがどの周波数帯しゅうはすうたいにいるかはペアリングに影響しない。

スマホが本当にブリッジを見つけているかを確かめるには、LAN 上の mDNS を聞くのが一番手っ取り早い。サーバー上でマルチキャストグループに参加して 5353 を聞く小さなプログラムを書いたところ、結果はこうだった。

text
スマホ が _matterc._udp を問い合わせ   ×30
サーバー の応答                        ×30
matter-hub が受けた接続                 0

スマホは30回問い合わせ、サーバーも30回応答しているのに、スマホからの接続は一度もない。つまり第1関門は通っていて、問題はその先にある。

TIP

avahi が入っていれば、avahi-browse -rt _matterc._udp でもブリッジがアドバタイズしている内容が見られる。次の節で使う TXT フィールドも含めて。

第2関門:Google の VID/PID チェック#

サーバーの応答を分解してみると、TXT レコードに VP=65521+32768 がある。16進数に直すと Vendor ID 0xFFF1、Product ID 0x8000。0xFFF1 は Matter がテスト用に予約しているベンダーコードで、matter-hub のように CSA の認証を受けていないプロジェクトはみなこれを使っている。

Google の開発者ドキュメントによると、Google Home で開発中の Matter デバイスをペアリングするには、先に Google Home Developer Console で Matter integration を作り、VID と PID をデバイスと一致させる必要がある。テスト用の VID は、アライアンスが割り当てた 0xFFF1 から 0xFFF4 の中から選べる。

matter-hub がアドバタイズしているのは 0xFFF1 / 0x8000 で、僕のアカウントには integration が1つもなかった。

NOTE

matter.js のドキュメントには、Google は未認証デバイスに対して「未認証デバイスのペアリングを許可する」確認ダイアログを出すと書いてあるが、それは古いバージョンの挙動だ。僕が実際に遭遇したのは、確認ダイアログも何もなくいきなり「デバイスが見つかりません」が出るパターンだった。

登録の手順。

プロジェクトを作る

Google Home Developer Console に Google Home アプリと同じアカウントでログインし、プロジェクトを作成する。

Matter integration を追加する

Add Matter integration をクリック。初回は説明ページが出るので、Next: Develop、Next: Setup と進む。

製品情報を入力する

Product name は好きに付けていい。Device type は Outlet(On-Off Plug-in Unit)を選ぶ。

VID と PID を入力する

Vendor ID は Test VID の 0xFFF1 を選び、Product ID に 0x8000 を入れる。ブリッジがアドバタイズしている値と一致させること。

保存してアプリに戻りもう一度スキャンすると、画面が「このデバイスが近くで検出されました」に変わった。これでこの関門は突破。

第3関門:IPv6 接続#

続行を押すと、また「デバイスが見つかりません」。

今回も matter-hub のログは空。まず1つの可能性を潰した。スマホが問い合わせを繰り返していたので、無線 AP が有線側のマルチキャストを捨てているのではと疑い、サーバーの応答をスマホに直接ユニキャストする小さなプログラムを書いて122回送ったが、やはり接続は来なかった。つまりスマホはレコードを確かに受け取っている。

残る未検証の部分は接続そのものだ。アドバタイズに載っているのは IPv6 アドレスだけで(Matter の仕様で IPv6 を使うと決まっている)、スマホはこのアドレスで UDP 5540 に接続する。以前、同じセグメントの NanoKVM から ping6 でこれらのアドレスを試したときはすべて応答があったので、ここはそれ以上調べていなかった。

そこで TCP で試すことにした。HA の 8123 をターゲットにして、同じマシン、同じアドレスに対して ping、IPv4、IPv6 でそれぞれ接続してみる。

bash
ping -6 -c 3 <サーバーのIPv6アドレス>
curl -4 -m 5 -s -o /dev/null -w '%{http_code}\n' 'http://<サーバーのIPv4アドレス>:8123/'
curl -6 -m 5 -s -o /dev/null -w '%{http_code}\n' 'http://[<サーバーのIPv6アドレス>]:8123/'

結果:

text
ping6        → 疎通OK(0.6ms)
IPv4 8123    → 200
IPv6 8123    → Connection timed out

IPv6 の ping は通るのに、TCP はタイムアウトする。タイムアウトと connection refused は別物で、refused は相手が拒否したということ、タイムアウトはパケットが捨てられて何の応答もないということで、たいていはファイアウォールが原因だ。

原因:ufw の LAN 向けルールが IPv4 しかなかった#

このサーバーは を使っていて、外から入ってくる接続はデフォルトですべてブロックしている。1週間前、デスクトップから HA の画面に入れなかったときに、こんなルールを追加していた。

bash
sudo ufw allow from 192.168.50.0/24 to any port 8123 proto tcp comment 'HA UI from LAN'

送信元に IPv4 のサブネットを指定しているので、ufw は IPv4 のルールしか作らない。sudo ufw status verbose で見ると一目瞭然いちもくりょうぜんだ(抜粋)。

text
22/tcp               ALLOW IN    Anywhere
8123/tcp             ALLOW IN    192.168.50.0/24    # HA UI from LAN
22/tcp (v6)          ALLOW IN    Anywhere (v6)

送信元を指定していない Anywhere のルールにはすべて対応する (v6) があるのに、送信元を LAN に限定したルールには1つもない。言い換えると、このマシンは IPv4 では LAN に必要なポートを開けているが、IPv6 では LAN に対してポートを1つも開けていなかった。

ping6 が通るのは、ufw のデフォルトルールが ICMPv6 を許可しているから(IPv6 の近隣探索きんりんたんさくに必要)で、mDNS もデフォルトルールで許可されている。だから探索も ping も正常で、実際に接続を張る TCP と UDP だけがブロックされていた。そして Google Home はこの状況も「デバイスが見つかりません」として表示する。

直し方#

ルールを2つ追加して、LAN からの IPv6 も IPv4 と同じように入れるようにした。

bash
sudo ufw allow from fc00::/7  comment 'LAN IPv6 (ULA)'
sudo ufw allow from fe80::/10 comment 'LAN IPv6 (link-local)'

fc00::/7 は のプライベートアドレス、fe80::/10 は で、どちらもインターネットからルーティングされて入ってくることはない。追加したあと、IPv6 での 8123 への接続はタイムアウトから 200 に変わり、もう一度ペアリングしたら成功した。

fe80::/10 は省略できない。ペアリング成功後のログを見ると、スマホと Google TV が matter-hub と張った2本の接続は、どちらも送信元が fe80:: で始まる link-local アドレスだった。ULA だけ開けた場合、ペアリングが終わってもやはりつながらない。

WARNING

この2行は、LAN 内のすべての IPv6 デバイスがこのマシンのすべてのポートに接続できるようにするもので、僕が IPv4 でポートごとに開けている範囲より広い。うちの LAN は自分のデバイスしかないのでこうしている。LAN に信頼できないデバイスがあるなら、この2つの範囲に対して Matter 用の 5540/5541 だけを開ける手もある。ただし fe80::/10 は必ず含めること。

ペアリングしたあと#

ペアリング成功後、さらにいくつかデバイスを追加した。フロアランプ、ドアベルの投光器、エアコン、除湿機じょしつき、空気清浄機、キャットタワーの電源で、最終的に8台。HA の script、automation、input_boolean は入れていない。Google Home ではコンセントの山になってしまうからだ。監視カメラのスイッチも、音声での誤作動ごさどうが怖いので入れていない。

その後、3つの問題に当たった。

音声では必ず部屋を言う必要がある#

初めて Google に「電気消して」と言ったら、消えたのはフロアランプとドアベルの投光器で、部屋の照明はそのままだった。

部屋の照明2つは HA では switch なので、matter-hub はこれをコンセント(OnOffPlugInUnit)として送る。一方 Google の「電気つけて」「電気消して」は、デバイスタイプが照明のものにしか効かない。Google Home アプリでデバイスを長押し → 設定 → デバイスタイプを「照明」に変更し、さらに各デバイスに部屋を割り当てる。

変更したら今度は別の問題が出た。全部照明になったので、「電気つけて」とだけ言うと、Google はフロアランプも母の部屋の照明もまとめてつけてしまう。なので実際には毎回「OK Google、僕の部屋の電気つけて」と言っている。部屋まで言えば、その照明だけが動く。

エアコンでブリッジ全体がクラッシュ#

エアコンを追加したら、bridge 全体が落ちて再起動を繰り返し、他のデバイスも巻まき込まれてオフラインになった。

text
climate_leng_qi.thermostat
minHeatSetpointLimit (1600) must be ≤ minCoolSetpointLimit (1600) − minSetpointDeadBand (200)
Failed to update bridge due to error: [constraint]

Matter のサーモスタット仕様では、暖房の下限が冷房の下限より少なくとも 2°C 低くなければならないが、うちのエアコンはどちらの下限も 16°C だった。設定はすでに data/ に書き込まれているので起動するたびにこれに当たり、管理画面にも入れない。仕方なくまずコンテナを止め、使い捨てのコンテナで bridge の設定ファイルを編集してエアコンを外してから起動し直した。

元のプロジェクトはアーカイブ済みなので、もう修正されることはない。RiDDiX の fork は 8 月にサーモスタットの温度デッドバンドの扱いを修正していて(#435)、その後も僕と同じ症状の報告が出ている(#454「Thermostat with equal min_temp for heat/cool crashes entire bridge process」)。移行は image を fork のものに差し替えるだけで、data/ はそのままでいい。fabric と node の ID も変わらないので、Google Home で再ペアリングする必要もない。差し替えたあと、エアコンは RoomAirConditioner として認識され、問題なく追加できた。

エアコンは絶対温度でしか指定できない#

「エアコンを1度上げて」は反応がなく、ログはこうだった。

text
Invoke « thermostat.setpointRaiseLower
Validation-Error 0x80: Missing mandatory field mode in field mode

このコマンドは仕様上モードと調整量を含める必要があるが、どうやら Google が送ってくるパケットにはモードのフィールドが欠けているようだ。「エアコンを 26 度に設定して」は目標温度の書き込みなので正常に動くし、エアコンのオン・オフも問題ない。今のところはこれで使っている。

切り分けの順番#

表にまとめておく。「デバイスが見つかりません」に当たったら、上から順に確認していけばいい。

確認項目確認方法僕の結果
スマホが mDNS の応答を受け取っているか5353 を聞く、または avahi-browse -rt _matterc._udp受け取っている、30問30答
ブリッジが使っている VIDTXT の VP フィールド、65521 が 0xFFF1テスト用 VID
Developer Console に登録してあるか同じ Google アカウントで、VID/PID が一致未登録、登録後「近くで検出」が出た
IPv6 の TCP/UDP が通るかcurl -6 で TCP サービスに接続する、ping だけで判断しないping は通る、TCP はタイムアウト
ファイアウォールに IPv6 の LAN 向けルールがあるかsudo ufw status verbose で (v6) があるか見るなし
link-local を許可しているかfe80::/10追加後にペアリング成功

今はベッドに寝転んだまま「OK Google、僕の部屋の電気消して」と言えば照明が消える。前回の RF スイッチとつながって、一通りの道のりは歩き終えた。ただし「部屋」の一言だけは省けない。

參考連結