プラグインの開発方法

プラグインの開発方法

2019年5月13日 Documentation 0

本稿では、PicoGWのプラグイン開発の概要を述べる。

概要

PicoGW本体(以後コアシステムと呼ぶ)の機能はAPIサーバーとプラグインの発見・管理のみに限られ、それ以外の機能はフロントエンドやネットワーク機能も含め、すべてプラグインによって実装されている。このためにコアシステムのソースコードは、ライブラリを除外すると0.5MBに満たない程度である(2018年4月現在)。これがPicoを標榜する理由でもある。
※ライブラリを入れると40MBくらいになるが、世の中そんなものであろう。

いずれにせよ、PicoGWと総称した場合、その機能のほとんどはプラグインで実装されているため、プラグインの実装を理解することで、PicoGWの機能を拡張したり、改変することができるようになる。

本ドキュメントに関する質問・要望等は、node-picogwのissuesにて受けつけているので、必要に応じて投げていただければ幸いである。英語推奨だが、日本語でも可である。

なお、早く実装例が見たいという方は、以下の実動するプラグインは実装がシンプルなのでご覧いただきたい。

localStorageを用いたデータベースプラグイン / server role (roleは後述)
https://github.com/KAIT-HEMS/node-picogw-plugin-db

名前付きパイプ / client role
https://github.com/KAIT-HEMS/node-picogw-plugin-namedpipe

プラグインの取り込み

PicoGWのプラグインはグローバルインストールされた通常のnpmパッケージとして提供される。コアシステムの実行開始時にグローバルインストール済みのパッケージをスキャンし、その名前が picogw-plugin- からはじまっているものがPicoGW用プラグインと認識され、とりこまれる。

PicoGWをインストールするといくつかのプラグインが同時にインストールされるが、APIサーバとして動作させるために本当に絶対に必要なのはadminプラグインだけである(他所ではwebやdb,macroもmandatoryであると書いているが、それはフロントエンドや、より上位レイヤーのサービスを用いる時に追加でプラグインをインストールするのが不便だから必須扱いにしたほうが利便性が高いと考えたまでで、実装上は正確ではない)。そのため、adminプラグインだけは特別扱いで最初に初期化され、その後に他のプラグインを初期化するようになっている。

role

プラグインにはその種別を表す role という属性値があり、プラグイン内に含まれるpackage.json内のpicogwキーの下で宣言する。roleはserver,client, httpから最低一つが用いられる。例えば、echonet pluginのroleは server 、macro pluginはserver,clientであり、web pluginはhttpである。

  • server属性とは最も通常のプラグインの機能、つまり何らかの通信プロトコルを実装して、WebAPIからアクセスできる機能を実装することを示している。
  • client属性とは、他のプラグインのAPIを内部的に呼び出すプラグインであることを示す。例えばmacroプラグインでは自動的に他のAPIのポーリングをして、家全体の電源状態を把握する機能を持っているため、この属性値を持っている。
  • http属性とはフロントエンドを作るプラグインであり、このプラグインからは設定画面を表示する機能をフックし、独自のUIを実装できるようになっている。

この種別を記述しておくことで、プラグインから利用できるPicoGWコアシステムの機能が限定されるのである。なお、種別は複数持つことができるが、server属性を持たないプラグインは自身のAPIをホストしないことになるため、設定画面の階層構造には出現しない。設定画面中にwebプラグインが表示されていないのはそのためである。

プラグインの基本的な構造

プラグインは、オブジェクトを一つmodule.exportsする必要がある。

module.exports = {
    init: function(pluginInterface){
    },
};

このオブジェクトは初期化コールバック関数を示すinitキーを必ず含む。
さらに、server roleを持つ場合はonCallコールバックを加える。

module.exports = {
    init: function(pluginInterface){
    },
    onCall: function(method, path, args){
    }
};

initコールバック

initはプラグイン初期化時に一度だけ呼ばれる。初期化に時間がかかりそうなときはPromiseを返すとよい。
引数はpluginInterfaceというオブジェクトで、roleに指定した機能をメソッドとして呼び出せるようになっている。
pluginInterfaceはinit()終了後も保管しておき、必要な時に利用する。

  • プラグイン内でのエラーメッセージ表示には pluginInterface.log を用いる。
  • プラグイン内でのlocalStorage利用には、pluginInterface.localStorageを用いる。~/.picogw/storage以下にプラグインごとに分類されて保存される。

pluginInterfaceには様々なメソッドが含まれているが、ドキュメンテーションは将来課題とする。ひとまずは他のプラグインをご覧いただくか、デバッガで止めながら機能を把握していただきたい。なお、 _ からはじまる名前のメンバーは直接アクセスしてはいけない。[ToDo]

onCallコールバック

onCallは、WebAPIクライアントからRESTに対応するCRUD系メソッドがコールされた時に呼ばれるコールバックである。引数はひとつめがmethod (‘GET’,’PUT’,’POST’,’DELETE’のどれか)で、2つめがpath(/v1/プラグイン名/を除いたリソース文字列)、3つめがargsである。たとえばHTTP GETで /v1/plugin1/a/b?p=q へのアクセスがあったとすると、plugin1プラグインのonCallの引数としては ‘GET’,’a/b’,{p:’q’} が渡ってくる。
このようにargsは、GETの場合はGET引数をJSONオブジェクトに展開したものとなり、それ以外の場合はBody内(JSON形式を想定)をJSONオブジェクトに変換したものが渡ってくる。

リクエストに応じた返答を作ってreturnするか、時間がかかる場合はPromiseを返し、返答ができるタイミングでresolveすればよい。

Publishについて

PicoGWにはPubSubの機能がある。PubSubは、serverプラグインで実装できる。プラグイン側からすべきことは、publishしたいタイミングで:

pluginInterface.server.publish(path, args);

を呼び出すだけである。pathには、/v1/プラグイン名/ を除いたパスを記述する。argsはクライアントに通知するJSONオブジェクトである。
メッセージのルーティングはコアシステムが行う。

APIペイロードの制約

APIの中身を流れるデータ、つまりペイロードの本体はプラグインによって実装されているため自由度は高いが、コアシステムとの連携上の制約・API階層をウォークスルーするための制約・PicoGW開発コンセプト上の理由などから本文書で述べる最低限のルールに従う必要がある。

  • リソース(パス名)に使える文字は 英小文字、 数字、 -_*+.() とする。
    大文字を使わないのはAPI使用者の混乱を避けるためである。例えばエアコンのことをAirConditionerと書くのか、airConditionerか、またはairconditionerと書くのかという問題である。全部小文字にすると視認性が悪くなるが、想起しやすく間違えにくいという利点があり、PicoGWではこの利点の方を重視することとした。ただし、プラグイン側で大文字指定をはじくべきだというわけではない。例えばAirConditionerと指定された時、これを小文字に自動変換して受け付けるか、エラーを返すかという判断はプラグインに任されている。

  • リソース末尾のスラッシュの有無によって機能を切り替えてはいけない。
    これもAPI使用者の混乱を避けるためで、/v1/plugin1/a//v1/plugin1/a は同じ意味に解釈しなければならないというルールである。

  • APIの返答は常にJSONオブジェクトであり、HTTP RESTでアクセスする場合のステータスコードは200である。
    PicoGWにはHTTP REST APIとWebSocket APIがあり、プラグイン実装時にはこれらを区別しない。エラーが起こった時に、その内容はHTTPのステータスコードで表しきれないことが多いため、常に返答はJSON Objectとし、その内容を見て判断することとする。そもそも返すべきステータスコードをプラグイン側から指定する機能はない。
    なお、エラー時の返答オブジェクトの形式は:

{
    "errors": [ {"error":"Error1 msg"} ]
}

のように、エラーオブジェクトの配列とする。各エラーオブジェクトのerrorキーは必須で、その値は文字列とする。

  • 引数に info=’true’ (trueは真理値ではなく文字列)が含まれる GETアクセス への正常時返答はAPI階層をウォークスルーするための情報である。
    API階層をウォークスルーするための情報とは:
    ① その階層がleaf(リソースの末端)であるか否かどうかということ
    ② leafでない場合は、次の階層のリソース名一覧
    ③ 自然言語によるリソースの説明文
    それに加え、設定情報をGUIから入力できるようにしたり、テスト機能を実現するためのメンバーがある。
    ④ settings
    ⑤ settings_schema
    ⑥ test
    である。
    ①と③は _info というキー内に格納される。 _info は以下の形式をとる。
"_info": {
    "leaf": true|false,    // ① 
    "doc":{                // ③
        "short": "One-line description",
        "long": "Long description"
    }
}

これらすべて省略可である。もし_infoが含まれない場合、このノードはleafであり、説明文は存在しないと解釈される。_infoがあるがその中にleafキーがない場合もleafであると解釈される。docも省略可である。つまり、GET引数に info:’true’ があっても返答に変化がないとすれば、それはリーフノードであると解釈される
②は、例えば /v1/plugin1/a/a , /v1/plugin1/a/b , /v1/plugin1/a/c という3つのリソースが存在する時、 /v1/plugin1/a への、引数に info=’true’ を与えられた GETアクセス への返答は、その下位に3つのリソースがあるということを示すためにキー a,b,c を含まなければならない。そして、返答のルート、および下位リソース情報内の両方に、前項で述べた _info のキーがあるものと期待される。例えば、このようになる。

{
    "a": {"_info":{"leaf":true}},
    "b": {}, // _infoを省略すると、leafであると解釈される
    "c": {
        "_info":{
            "leaf":false
            ,"doc":{"short":"Resource c is not a leaf."}
        }
    },
    "_info":{ "leaf":false }
}

なお、上記の例からもわかると思うが、下層リソースのオブジェクトがリーフでない場合、そのさらに次の層のリソース一覧を含む必要はない。

プラグイン運用ルール

PicoGWで重要視していることは、常に上位互換性を保つということである。なぜなら、PicoGWのように建物や物体に紐付いたようなシステムは長期運用が想定されるので、某インターネット企業のように数年でAPIが使えなくなったりすると大きな問題が生じる。テクノロジーの進歩速度は増す一方だが、物質世界の変化はそんなに早くない。せっかく予算を投入してビル管理システムを開発し、いざこの建物を50年使おうと思っても、そこで用いるソフトウェアが5年で使えなくなるようでは困るのである。従って、プラグイン開発者は互換性がなくなるような変更を厳に慎むべきである。

具体的には、ひとたびnpmにプラグインをリリースしたら、以下の変更は禁止とする。

  • 正常返答に含む情報を減らす、または改変すること
    例えば、あるリソースへのアクセス時、最初はタイムスタンプが入っていたがどこかでタイムスタンプを含まないようにしたり、タイムスタンプのフォーマットを変更することは許されない。

  • 正常返答できていたリクエストがエラーを返すようになること
    例えば、当初airConditionerとしてアクセスできていたものが、小文字のみ受け付けるようになってエラーが返るような変更は許されない。

逆に、次の変更は許される。

  • 返答に含む情報を増やすこと
    例えば返答に含まれていなかったキーを増やすことは問題ない。

  • エラーが返っていたものが正常処理するようになること
    例えば、/v1/plugin_1に対するアクセスを、一旦エラーにしておいて公開を先行させ、実装が進んだら正常な返答を返すような運用は可能である。

もしどうしても上位互換性を失うような変更をしたい場合は、新しいプラグイン名を作り、そのAPIとして新しいペイロードを返すようにすべきである。このときも、旧プラグインは残され、メンテンナンスされ続けなければならない。

この方針は、npmパッケージとして登録する時に守っていただきたいものである。従って、APIを吟味しつつプラグイン開発をしている最中はnpmパッケージとして登録・公開すべきではない。幸いnpmにはtarballやローカルディレクトリなどからパッケージをグローバルインストールすることもできるので、APIが落ち着いた時点でnpmにアップすればよいのである。もし開発中にnpm上での名前を取られてしまう可能性を気にする場合は、エラーだけ返すダミープラグインを、希望する名前でnpmに先行して登録しておくとよい。


日本語ドキュメントのトップに戻る