Raspberry PiをMatter対応デバイスにしてAmazon Alexaから操作する


## はじめに

Matterとは近年新しくできたスマートホームの標準規格で、従来は各社まちまちだったスマートスピーカーやスマート家電間の通信方式を共通化して、さらにスマートホームを便利にしようというものです。

Matterに対応したデバイスはその機能を調光照明や接触センサーといったカテゴリに抽象化され、同じくMatterに対応したスマートスピーカーなどから簡単に接続し操作することが可能になります。


ここではRaspberry PiにMatterデバイスとしての機能を追加し、Amazon Echo(Alexa)から操作することを目指します。

ライブラリとしては公式のC++実装である[connectedhomeip](https://github.com/project-chip/connectedhomeip)を使うのが基本となりますが、これとは別に同じく公式のTypeScript実装である[matter.js](https://github.com/project-chip/matter.js)もありますので、今回はこちらを使用します。

Matterデバイスが使用する通信方式はBLE・Wi-Fi・Threadsなどから選択できますので、最も手軽なWi-Fi(LAN)を介してRaspberry PiとEchoを接続します。
有線・無線を問わず同一LAN内に接続されていれば良いので、Raspberry Piに限らずPCでもNode.jsが動く環境であれば同じ方法でMatterデバイスとして動作させることができます。


## 環境

* Amazon Echo Dot 第4世代

* Raspberry Pi 3 Model A+  
Raspberry Pi OS Lite 64bit (bullseye)

* Windows 10 + WSL1 (Ubuntu 22.04)  
最終的にはRaspberry PiをMatterデバイスとして動作させますが、開発段階ではPC(WSL)でテストしていました。

* リポジトリ：[matter.js](https://github.com/project-chip/matter.js)  
v0.7.5-alpha.0-20240207-b8ba7c4f
(commit 0fb55780a0b260ed04dfcce718b3d2d8e0d13458)

https://github.com/project-chip/matter.js

## 準備
Node.js、npm、gitをMatterデバイスにしたい機器（今回はRaspberry Pi）にインストールします。

例えば以下のようなコマンドになりますが、環境によってはより良いインストール方法があったりするので、"（環境名） Node.js"とかで検索するのが良いかと思います。
```
sudo apt install git nodejs npm
```

インストールできているか確認します。
```
$ node --version
v18.18.2
$ npm --version
9.8.1
$ git --version
git version 2.30.2
```


## matter.jsのインストール

Raspberry Pi内に作業用ディレクトリを作り、そこにmatter.jsのリポジトリをcloneします。
```
git clone --recursive https://github.com/project-chip/matter.js.git
```

リポジトリのディレクトリに入って
```
cd matter.js
```

インストールすればOK！
```
npm install
```

…と言いたいのですが、環境によってはうまくインストールできない場合があります。

以下のようにオプションを追加することでインストール可能になる場合もあります。（verboseはログレベルを変更するオプションなので無くてもいいです）
```
npm install --unsafe-perm --verbose
```

しかし、Raspberry Pi 3A+では上記コマンドでもうまくいかず、インストールが途中で全く進まなくなってしまいました。どうやら、メモリが不足しているようです。


## matter.jsのインストール（メモリ容量の少ないデバイスの場合）

メモリの状態を確認するには`free`コマンド、swapの状態を確認するには`swapon`コマンドを使用します。
```
$ free
               total        used        free      shared  buff/cache   available
Mem:          429392       66388      118012          40      244992      306532
Swap:         102396       14452       87944
$ swapon -s
Filename                                Type            Size    Used    Priority
/var/swap                               file            102396  14452   -2
```
swapが100MBなので、ちょっと物足りないですね。

Raspberry Piでswapを拡張するには、まずswapを管理しているサービスを止めて、
```
sudo service dphys-swapfile stop
```
設定ファイルを書き換えます。
```
sudo vi /etc/dphys-swapfile
```

標準では
```
CONF_SWAPSIZE=100
```
のように記述されているので、これを書き換えます。
```
CONF_SWAPSIZE=2048
```
この場合swapサイズは2GBになります。他の環境だと適切なサイズは異なるかもしれません。

ファイルの書き換えが完了したら止めていたサービスを再起動します。
```
sudo service dphys-swapfile start
```

swapサイズが変更されているか確認します。
```
$ free
               total        used        free      shared  buff/cache   available
Mem:          429392       76600      100104        1048      252688      295272
Swap:        2097148           0     2097148
$ swapon -s
Filename                                Type            Size    Used    Priority
/var/swap                               file            2097148 0       -2
```

ここで再度matter.jsのディレクトリ内で`npm install`を実行すると…

```
<--- Last few GCs --->

[16445:0x9a9a870]   100418 ms: Mark-sweep 250.7 (258.6) -> 249.6 (258.6) MB, 547.2 / 0.0 ms  (average mu = 0.261, current mu = 0.152) allocation failure; scavenge might not succeed
[16445:0x9a9a870]   100989 ms: Mark-sweep 250.7 (258.6) -> 249.7 (258.8) MB, 526.4 / 0.0 ms  (average mu = 0.207, current mu = 0.078) allocation failure; scavenge might not succeed


<--- JS stacktrace --->

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
 1: 0xb7f974 node::Abort() [node]
 2: 0xa97f08  [node]
 3: 0xd40e78 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [node]
 4: 0xd41048 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [node]
 5: 0xf1f5fc  [node]
 6: 0xf20264 v8::internal::Heap::RecomputeLimits(v8::internal::GarbageCollector) [node]
 7: 0xf307ac  [node]
 8: 0xf31370 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node]
 9: 0xf0d718 v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node]
10: 0xf0e6f0 v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node]
11: 0xef1380 v8::internal::Factory::NewFillerObject(int, v8::internal::AllocationAlignment, v8::internal::AllocationType, v8::internal::AllocationOrigin) [node]
12: 0x1297a34 v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [node]
13: 0x167c4ec  [node]
Aborted
```

駄目でした。ヒープのサイズが足りないようです。これも設定を変えてやる必要があります。ちょっと大きめの値にすると良さそうです。

というわけで、最終的にはswapを確保したうえで以下のようなコマンドを使用することでインストールに成功しました。
```
NODE_OPTIONS="--max-old-space-size=512" npm install --unsafe-perm --verbose
```


## サンプルを実行し、Amazon Echo (Alexa）と接続する

サンプルのディレクトリに移動していずれかのサンプルを実行します。

例えばBridgedDevicesNode.tsには、一つのブリッジデバイスが複数のMatterデバイスをまとめて管理しているような構成が記述されていますので、以下のようにコマンドパラメータを与えます。
```
cd packages/matter-node.js-examples
npm run matter-bridge -- -num 2 -on1 "echo onon1" -off1 "echo offoff1" -type2 socket -on2 "echo onon2" -off2 "echo offoff2"
```

実行するとコンソール画面にQRコードが表示されます。（CUI画面にQRコードが表示されるというのも、初見時はちょっとびっくりしますね。実現方法は至ってシンプルなのですが「なるほど」となりました。）
このQRコードを使用してスマートスピーカーとMatterデバイスをペアリングさせます。

QRコードがうまく表示されない場合は、同時にログに出力されるURLにブラウザからアクセスすることでもQRコードを表示させることができます。
または、同じくログに出力されるManual pairing codeを使用しての接続も可能です。


なお、このときRaspberry Piと同じLAN内にいるPCからmDNSでスキャンしてみると、MatterデバイスとしてRaspberry Piが検出されます。

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3714797/9522d9a6-f694-e145-5d63-baf789a1b1e3.png">


おそらくAmazon EchoのようなコントローラーはmDNSで通知される情報とQRコードで読み込んだデータを照合して、どのIPアドレスとポートに接続しに行けばいいのか判断しているのでしょう。


Amazon EchoとMatterデバイスを接続するには、スマホのAlexaアプリからデバイスの追加操作をおこないます。
アプリ下部の"デバイス"タブから右上の"+"ボタンを押して

<img width="40%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3714797/1f8e461e-0545-cda7-bf29-f2d5bb01f94f.png">

"デバイスを追加"

<img width="40%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3714797/f532279f-3832-7b00-f6d5-3de0673993eb.png">

最下部の"その他"から

<img width="40%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3714797/597ff485-95ca-60c1-2a70-e4bd97d05561.png">

Matterのロゴを選択し、あとは画面の指示に従ってQRコードを読み込みます。

<img width="40%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3714797/bf6ab354-4928-4c28-22c7-415661b23a75.png">


しばらく待つとコンソールにログが流れ、正常に接続が完了するとAlexaアプリのデバイス一覧にブリッジデバイス・照明・スマートプラグの3つのデバイスが追加されます。一回のペアリングで、ブリッジデバイスが管理する全デバイスが個別に操作可能になるわけです。
ここから照明やスマートプラグのON/OFFを操作すると、コマンド引数の"echo"で与えた文字列がコンソールに表示されます。つまり、後はechoの代わりに好きなシェルスクリプトでも叩くようにしてやれば好き勝手出来るということですね。


デバイス側でのペアリング情報はローカルのディレクトリ（デフォルトでは".device-node"）内に保存されるため、2度目以降の起動時には再度のペアリング操作は不要になります。逆に一度設定をリセットしたい場合はこのディレクトリを削除するか、コマンドに"clearstorage"オプションを追加して実行します。


## 調光機能付き照明

上記で使ったサンプル(BridgedDevicesNode.ts)ではON/OFFの操作が可能なデバイスが作成できますが、さらに調光機能の付いたデバイスを作成することも可能です。

OnOffLightDeviceやOnOffPluginUnitDeviceの代わりに、DimmableLightDeviceもしくはDimmablePluginUnitDeviceを使用します。

```ts
const initialAttributeValues: AttributeInitialValues<typeof LevelControl.Cluster.attributes> =
{
    minLevel: 0,
    maxLevel: 254,
    currentLevel: 0,
    onLevel: null,
    options: {
        executeIfOff: true,
        coupleColorTempToLevel: false,
    },
};
const device = new DimmableLightDevice(undefined, initialAttributeValues);
device.addOnOffListener(on => logger.info(`light onoff ${on}`));
device.addCurrentLevelListener(level => logger.info(`light level ${level}`));

const name = `DimmableLight1`;
aggregator.addBridgedDevice(device, {
    nodeLabel: name,
    productName: name,
    productLabel: name,
    serialNumber: `node-matter-${uniqueId}-dl`,
    reachable: true,
});
```

明るさの値（レベル）は0-254の範囲で通知されます。レベルの値に応じてコールバック内での処理を分岐させるようにすれば、照明の操作以外にもいろいろと応用ができそうですね。


## エアコン

冷房や暖房のような温度調整機器もMatterではサポートされているのですが、matter.jsではまだ非対応のようです。（2024/2現在）

過去には[プルリクエスト](https://github.com/project-chip/matter.js/pull/169)も上げられていたようですが、その後どうなっているのやら…？


おとなしく正式対応を待つのが賢明ですが、待ちきれないので上記プルリクを参考に温度調整部分だけ現在の仕様に合わせて再実装しました。

https://github.com/Boredbone/matter.js/commit/d8697003763d084631bc31de06b584468e8a8a36

```ts
const initialAttributeValues: AttributeInitialValues<typeof Thermostat.Complete.attributes> =
{
    localTemperature: 2500,
    minHeatSetpointLimit: 1000,
    maxHeatSetpointLimit: 4000,
    minCoolSetpointLimit: 1000,
    maxCoolSetpointLimit: 4000,
    occupiedCoolingSetpoint: 3000,
    occupiedHeatingSetpoint: 2000,
    minSetpointDeadBand: 1,
    controlSequenceOfOperation: Thermostat.ControlSequenceOfOperation.CoolingOnly,
    systemMode: Thermostat.SystemMode.Cool,
    thermostatRunningMode: Thermostat.ThermostatRunningMode.Cool,
};
const device = new ThermostatDevice(undefined, undefined, undefined, initialAttributeValues);
device.addOnOffListener(on => logger.info(`thermostat onoff ${on}`));
device.addSystemModeListener(mode => logger.info(`thermostat mode ${mode}`));
device.addOccupiedCoolingSetpointListener((v: any, o: any) => logger.info(`thermostat temperature ${o} -> ${v}`));
device.addOccupiedHeatingSetpointListener((v: any, o: any) => logger.info(`thermostat temperature ${o} -> ${v}`));

const name = `Thermostat1`;
aggregator.addBridgedDevice(device, {
    nodeLabel: name,
    productName: name,
    productLabel: name,
    serialNumber: `node-matter-${uniqueId}-th`,
    reachable: true,
});
```

Alexaから温度設定を変更するとoccupiedCoolingSetpoint/occupiedHeatingSetpointが変化するので、この値に応じて何らかの処理をしてやります。
なお、温度の値は100倍された整数で扱われます。

ちょっといけてない点として、Alexaから音声で操作する場合にデバイス名を"冷房"や"暖房"にしているとON/OFF操作がうまくいきませんでした。エアコンの運転モード（Matter的にはsystemMode）を表す"冷房"や"暖房"と被っているのがよろしくないようで、デバイス名を"冷却"とか"ヒーター"とかのちょっとずらした名前にしてやると音声でのON/OFFも良好に動作します。


他のまだmatter.jsが対応していない種類のデバイスについても、同じように自前で実装してやれば対応可能なはずです。

ただし、Matterの仕様で定義されているデバイス種別の全てがAlexaから操作可能というわけではないようです。
各種スマートスピーカーがMatterのどの機能をサポートしているかは以下にまとめられています。

https://github.com/project-chip/matter.js?tab=readme-ov-file#device-types-tested-on-various-ecosystems



## おわりに
AlexaからRaspberry Piを操作する方法としては、他にもAlexaスキルを作ってMQTTで通信して…みたいな方法もあるのですが、Matterを使うと構成がシンプルになりますし、何より追加の開発なしに他のスマートスピーカーにも接続できる（はず）というのは魅力的です。

スマートホームデバイス開発のコストが下がるということで、今後は自作に限らず各社からMatter対応の新製品が多数発売されて生活がもっと便利になる…という未来に期待したいですね。