3プロトコルの機器を1つの名前で操作する自作CLI casa
家じゅうの機器を、1台のマシンから叩けるコマンドで動かしています。ただ、機器はプロトコルごとに「呼び方」がまるで違います。
たとえばリビングのエアコンをONにしたいとき、ECHONET Lite ならこう書きます。
enl set 192.168.213.10 0x013001 0x80 0x30
192.168.213.10 が機器のIP、0x013001 がその機器を指すオブジェクト識別子(EOJ)、0x80 が電源プロパティ(EPC)、0x30 がON。IPも16進のコードも、覚えていないと1文字も書けません。
これがMatterのスマートプラグになると、まったく別の書き方になります。
mat on 1234
1234 はペアリング時に振られたノード番号。今度はIPもEOJも出てきません。SwitchBotならさらに別で、クラウドAPIにデバイスIDを渡します。
同じ「ONにする」なのに、プロトコルごとに覚えることが違う。これを毎日やるのは無理です。そこで、機器に人間が読める名前を1つ付けて、どのプロトコルでも同じ動詞で叩けるようにする横断CLIを作りました。それが casa です。GitHubで公開しています(github.com/nogu3/casa)。
上の3つは、casa ではこうなります。
casa on bedroom_light
casa on living_light
casa on entry_lock
IPも、16進も、ノード番号も出てきません。名前と動詞だけ。この記事では、その中身を実際のコマンドと出力付きで書いていきます。
なお、これは自作Rust製ツール群の全体像をまとめた記事の、オーケストレーション層の深掘りにあたります。
casaが「しない」こと
先に、casa が何をしないかを書きます。ここが設計の背骨だからです。
casa はプロトコルを一切喋りません。バイト列も、UDPソケットも、HTTPの認証も持ちません。ではどうやって機器を動かすのか——すべて兄弟CLI(enl / mat / swb)をサブプロセスとして呼び、そちらに丸投げします。
つまり casa on bedroom_light は、内部でこう変換されて子プロセスに渡っているだけです。
casa on bedroom_light
↓ 設定ファイルで bedroom_light は echonet / 192.168.213.11 / EOJ 0x029101 だと解決
enl set 192.168.213.11 0x029101 0x80 0x30
casa 自身の仕事は3つに絞ってあります。
- 名前解決 —
bedroom_lightという名前を(プロトコル, アドレス, オブジェクト)に引き当てる - 設定の読み込みとバリデーション — 機器の定義ファイルを読み、壊れていないか検証する
- 一貫したラッパUX — どのプロトコルでも同じ動詞・同じJSON形で叩けるようにする
これだけです。プロトコルごとの難しさは、それぞれの専用CLIの中に閉じ込めたまま。だから mat の実装をまるごと入れ替えても、casa 側は1行も直さずに済みます。
ただし「薄い」のと「プロトコルをまたぐ操作ができない」のは別の話です。複数機器をまとめて動かすグループ化や、時刻・イベント起点のルールベース実行は、どのプロトコルの組み合わせでも対象です。たとえば「enl の listen(ECHONET LiteのINF通知)を検知したら mat の照明を起動する」のような、プロトコルをまたいだ連携も書けます(詳しくは後述のグループと casad の節で扱います)。
まず、機器に名前をつける
名前解決の元になるのが設定ファイル devices.toml です(既定の置き場は ~/.config/casa/devices.toml)。1機器につき1ブロックで、名前と、その機器がどのプロトコルのどこにいるかを書きます。
version = 1
# ECHONET Lite の機器は IP と EOJ(機器を指す識別子)で位置を書く
[devices.living_aircon]
protocol = "echonet"
ip = "192.168.213.10"
eoj = "0x013001"
[devices.bedroom_light]
protocol = "echonet"
ip = "192.168.213.11"
eoj = "0x029101"
# Matter の機器はペアリング済みノードの番号で書く
[devices.living_light]
protocol = "matter"
node_id = "1234"
# SwitchBot はクラウドAPI用のデバイスID
[devices.entry_lock]
protocol = "switchbot"
device_id = "DUMMY-XX-XX"
(IP・ノード番号・IDはすべて例です。実物の設定は別リポジトリで管理して、ここに置くか symlink する運用にしています。)
現在アダプタがあるのは ECHONET Lite・Matter・SwitchBot の3プロトコルです。プロトコルごとに位置の書き方(IP+EOJ / ノード番号 / デバイスID)は違いますが、casa から見れば全部ただの「名前のついた機器」になります。
登録した機器は casa list で一覧できます。出力は常に純粋なJSONです。
casa list{
"timestamp": "2026-07-19T12:34:56+09:00",
"devices": [
{ "name": "living_aircon", "protocol": "echonet", "ip": "192.168.213.10", "eoj": "0x013001" },
{ "name": "living_light", "protocol": "matter", "node_id": "1234" }
]
}
stdout はいつも構造化JSONだけ。診断ログやエラーは stderr に、RUST_LOG で制御できる形で出ます。この分担は兄弟CLIと共通の規約で、そのまま jq に流せます。AIエージェントのツール実行に組み込む場合も、呼び出す先をこのCLIにするだけで、出力のパース処理を別に書かずに済みます。
動かす:統一された動詞
名前がつけば、あとは動詞で叩くだけです。日常でいちばん使うのは電源のON/OFFです。
casa on living_aircon
casa off living_aircon
プロパティの読み書きもできます。「今エアコンはついているか」を読むならこうです。
casa get living_aircon 0x80{
"timestamp": "2026-07-19T12:34:56+09:00",
"device": "living_aircon",
"protocol": "echonet",
"value": { "power": "on" }
}
ここで1つ、正直に書いておくべき癖があります。get / set の第2引数(何のプロパティか)はプロトコル依存です。ECHONET Lite ならEPC(0x80 のような16進コード)ですが、Matterでは endpoint/cluster/attribute という3段の指定になります。
# ECHONET Lite:EPC 0x80(電源)
casa get living_aircon 0x80
# Matter:エンドポイント1の onoff クラスタの on-off 属性
casa get living_light 1/onoff/on-off
casa はこのセレクタ自体を解釈しません。/ で割って子CLIに渡すだけで、正しいかどうかは mat 側が判断します。プロトコルの差を無理に隠さず、「同じ意味を持つ操作」だけ統一動詞に昇格させる——この線引きが次の話です。
動詞を増やしすぎない:昇格の基準
on / off / get / set / describe は casa の専用サブコマンドです。でも、たとえば照明の色温度を変える color-temp は専用サブコマンドにしていません。ここには明確な基準を置いています。
専用サブコマンドを足すのは、2つ以上のプロトコルで同じ意味を持つ操作か、日常で頻繁に使う操作だけ。
on(電源ON)はECHONET LiteでもMatterでもSwitchBotでも意味が通るので昇格。一方 color-temp は事実上Matterの照明専用で、この基準を満たしません。こういう「裾野の長い」操作は、汎用動詞 invoke に流します。
casa invoke living_light color-temp --kelvin 2700
invoke は、コマンド名より後ろをそっくり子CLIに素通しします。上の例なら裏で mat color-temp --node 1234 --kelvin 2700 が走るだけ。--kelvin と --mireds の排他チェックや範囲のクランプは mat や機器の責任で、casa は解釈しません。
こうしておくと、casa の動詞は「本当に横断的なものだけ」の小さな集合に保てます。プロトコル固有の細かい操作でCLIが膨れないし、新しい操作が増えても invoke で拾えます。実際、色温度専用の casa color-temp は一度作ったあと、この基準に照らして削除しました(v0.6.0)。動詞を増やす方向ではなく、減らす方向に自制をかけているわけです。
グループで、プロトコルをまたいだシーンを1コマンド
横断CLIならではの機能がグループです。設定ファイルで複数の機器を1つの名前に束ねられます。
[groups.living]
members = ["living_light", "living_aircon"]
すると、casa on living の一発で、Matterの照明(living_light)もECHONET Liteのエアコン(living_aircon)もまとめて、並列にONになります。プロトコルが混ざっていても構いません。子CLIを全メンバー分いっぺんに起動して走らせます。
結果はメンバーごとのJSONで返ります。
{
"timestamp": "2026-07-19T12:34:56+09:00",
"group": "living",
"results": [
{"device": "living_light", "protocol": "matter", "ok": true, "value": {}},
{"device": "living_aircon", "protocol": "echonet", "ok": false,
"error": {"kind": "child_failed", "exit_code": 3, "detail": "..."}}
]
}
全員成功なら終了コード0、1台でもコケたら15(group_partial_failure)。部分的にコケても、誰がどの終了コードで失敗したかがJSONに残るのがポイントです。「照明はついたけどエアコンはタイムアウトした」を後から機械的に切り分けられます。
ただしグループには制約もあります。get / set はプロパティの指定がプロトコル依存なので、混在グループに投げると片方には意味不明なセレクタが飛びます。実用になるのは同一プロトコルのグループだけ、と割り切っています。
自動化は casad に切り離す
「エアコンがついたらリビングの照明もつける」「22時になったら消す」——こういう自動化は、あえて casa 本体には入れていません。casa は状態を持たない一発実行の道具のまま置いておきたいからです。
代わりに、同じワークスペースの別バイナリ casad(末尾のdはデーモンのd)が担います。ルールはTOMLで書きます。
version = 1
# イベント起動:living_aircon(ECHONET Lite)の電源(EPC 0x80)が ON(0x30)になったら、
# living_light(Matter)をつける——プロトコルをまたいだ連携
[[rules]]
name = "living light ON when aircon starts"
when = { device = "living_aircon", epc = "0x80", equals = "0x30" }
then = { action = "on", device = "living_light" }
# 時刻起動:毎日22時にリビングの照明を消す
[[rules]]
name = "living light off at 22:00"
when = { at = "22:00" }
then = { action = "off", device = "living_light" }# ルールを検証(実機は叩かない)
casad check rules.toml
# 常駐起動(時刻スケジューラと、イベント待ち受けを同時に走らせる)
casad run rules.toml
イベント起動は、ECHONET Liteの自発通知(INF=機器が状態変化を勝手に知らせてくるパケット)を待ち受けることで実現しています。時刻起動は内蔵スケジューラ。そして発火したら、casad は自分で機器を叩かず、casa を子プロセスとして呼ぶだけです。
この分け方のおかげで、casa 本体は最後までステートレスなまま。常駐・状態・スケジューリングという「重い関心事」は casad 側に隔離されています。デバッグ用に時刻トリガーを1回だけ評価する --once --now 22:00 もあり、毎分のcronから委譲する使い方もできます。
何が嬉しかったか
作ってからまだ1、2週間ですが、この横断層が効いていると感じているのは主に3点です。
- AIも人も、同じ名前で家を触れる —
casaは構造化JSONを吐くので、AIはCLIを直接叩けます。家族が使うWebフロント(mando)も、裏でcasaを呼ぶだけ。人間用のUIとAI用の入口を別々に作らずに済みます。 - 差し替えが設定で済む — 照明を無線アダプタでECHONET Lite化したときも、変えたのは
devices.tomlの1ブロックだけ。上の層のコードは無傷でした。 - 失敗の切り分けが効く — 子CLIの終了コードをそのまま伝播させるので、
casa get ...; echo $?の値だけで「タイムアウト(3)なのか、そもそも名前が設定にない(11)のか」を区別できます。
小さなCLIをプロトコルごとに分けて、その上に薄い名前の層を1枚だけ乗せる。市販の統合ソフトのように全部を1つの大きなアプリに畳み込まない代わりに、境界が明確で、どこが壊れているかがすぐ分かる。UNIX哲学をそのまま家に持ち込んだ結果が、この casa です。
コードは全部公開しているので、READMEの実行例からそのまま手を動かせるはずです。
casa— github.com/nogu3/casa- 全体像のハブ記事 — 自作Rust製ツール6本でスマートホームを一元管理する全体構成
enl や mat が実際に機器とどうやり取りしているかは、それぞれの記事(ECHONET Liteとは?、chip-toolのCPU100%が限界、Matterコントローラ自作へ)で書いています。次は、まだ記事にしていない ais の役割と設計思想を書いていく予定です。