家じゅうの機器を、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行も直さずに済みます。

ただし「薄い」のと「プロトコルをまたぐ操作ができない」のは別の話です。複数機器をまとめて動かすグループ化や、時刻・イベント起点のルールベース実行は、どのプロトコルの組み合わせでも対象です。たとえば「enllisten(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 / describecasa の専用サブコマンドです。でも、たとえば照明の色温度を変える 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の実行例からそのまま手を動かせるはずです。

enlmat が実際に機器とどうやり取りしているかは、それぞれの記事(ECHONET Liteとは?chip-toolのCPU100%が限界、Matterコントローラ自作へ)で書いています。次は、まだ記事にしていない ais の役割と設計思想を書いていく予定です。