Node-REDでWeb of Thingsを試してみる

## はじめに

私は、[node-red-node-wot](https://www.npmjs.com/package/@thingweb/node-red-node-wot)の開発に参画しています。これは、Node-RED上でWeb of Things(WoT)を用いたIoTシステムを開発できるオープンソースソフトウェアです。
本記事では、この`node-red-node-wot`の使い方を紹介します。

## 背景

IoTシステムは、多様なデバイスが混在する複雑な環境で運用されることが一般的です。このような環境では、デバイスごとに異なる通信プロトコルやデータ形式を統合するのが難しく、新しいデバイスの追加や既存デバイスの置き換えが発生するたびに、システムの再設計や再構築が必要になることが少なくありません。

[WoT](https://www.w3.org/WoT/)は、デバイスとデバイスを統合的に管理する装置やプログラムのやり取りに関する標準仕様です。WoTでは、デバイスをThingと呼び、デバイスを統合的に管理する装置やプログラムをConsumerと呼びます。
WoTの利点の一つは、Thingの機能や通信方法をJSON形式で記述するThing Descriptionを使うことで、ThingとConsumer間での標準化されたやり取りが可能になる点です。これにより、例えば新しいセンサーに変更する際も、Thing Descriptionを変更するだけで簡単にConcumerと連携できるため、柔軟性と拡張性が向上します。
WoTの詳細については以下を参照ください。

https://wot-jp-cg.netlify.app/docs/about

WoTを実装するための代表的なプロジェクトに、[Eclipse Thingweb](https://www.thingweb.io/)があります。このプロジェクトでは、WoTの標準仕様に基づくコンポーネントが提供されており、その一部として、Node-RED上でThingやConsumerを簡単に実装できる`node-red-node-wot`が開発されています。

Node-REDは、ノーコード/ローコードツールであり、ノードと呼ぶソフトウェア部品を接続したフローを定義することで処理を作成します。公開されているノードにはセンサーなどのデバイスを制御するものが多数あります。また、Node-REDは、Raspberry Piのような小型のコンピュータでも動作するため、IoTシステムと相性が良いです。

## 作成するThing

今回は実際のデバイスは利用せず、プログラムから以下の操作を行える架空の温度センサーを例にThingを作成します。

* 計測温度の取得
* 計測温度がしきい値を超えた場合に警告を出力
* 警告のためのしきい値温度を設定

実際は、デバイスをRaspberry Piに接続し、そのRaspberry Piで稼働するNode-REDからデバイスを制御する構成を想定しています。

Thing Descriptionには、Property、Action、Eventの3種類のアフォーダンスを定義できます。詳細は以下を参照ください。

https://wot-jp-cg.netlify.app/docs/td#thing-description%E3%81%AE%E6%A7%8B%E6%88%90

今回のダミー温度センサーでは、各アフォーダンスとして以下の機能を実装します。

* Property
    * temperature: 計測温度の公開
* Action
    * setTempThreshold: 警告のためのしきい値温度の設定
* Event
    * tempAlert: 計測温度がしきい値を超えた場合に出力する警告

## 環境構築

### 前提

* nodejs v18以上

### Node-REDのインストールと起動

以下ページを参照し、Thingを実行するPCにNode-REDをインストールします。

https://nodered.jp/docs/getting-started/local

私の環境(Mac)では以下コマンドでインストールしました。

```shell
$ npm install -g --unsafe-perm node-red
```

以下のコマンドで起動します。

```shell
$ node-red
```

起動後、Webブラウザから以下のURLにアクセスするとNode-REDのエディタが表示されます。
http://localhost:1880/

### node-red-node-wotの追加

Node-REDエディタの右上のハンバーガーメニューを開き、`パレットの管理`をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/188d7c0f-eed4-a9c8-bba2-9e32e9fc3ebd.png)

`ノードを追加`タブをクリックし、検索欄に`@thingweb/node-red-node-wot`を入力すると該当するノードが見つかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/32db7d05-db69-d74c-5642-474707634421.png)

`ノードを追加`をクリックすると以下のダイアログが表示されますので、内容を確認し、`追加`をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/c265d732-c2fd-ec04-0842-7f8f5c5b8d6d.png)

しばらく待つとノードの追加が完了し、以下のダイアログが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/efa9344d-d10c-c36c-a807-7f7bf79c88bf.png)

Node-REDの左側のパレットには、`Web of Things`のノード群が追加されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/9ea77f45-8023-1704-c4fe-5c6841de1387.png)

ノードの追加が完了したら、Node-REDエディタを表示するブラウザ画面のリロードを実施してください。

以上で、Thingを作成する準備は終わりです。

## Thingのフロー作成

### Thingの公開方法の設定

Server-Propertyノードをキャンバスに配置します。
そのノードをダブルクリックし、プロパティ画面を表示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/bffe2ff6-ef69-155c-ccdf-d9e56a6d76cc.png)

サーバ設定とThing設定は、Thingを構成するServer-Property、Server-Action、Server-Eventノードで共通的に参照する設定です。
サーバ設定の右のペンアイコンをクリックし、サーバ設定画面を表示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/658f8508-2b4f-709d-7d67-8b9d861d5992.png)

サーバ名、Binding種別、Binding設定を以下のように設定します。

* サーバ名: `http-server`
* Binding種別: `http`
* Binding設定: `{"port":8080, "allowSelfSigned":true}`

Bindingとは、ThingとConsumerがやり取りする手段(プロトコル)を実装したモジュールです。
上記の設定より、このThingをHTTPサーバ(ポート8080)で公開します。

設定が完了したら、更新ボタンを押して保存します。

プロパティ画面に戻るので、今度はThing設定の右のペンアイコンをクリックし、Thing設定画面を表示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/ba44f31c-aa17-4087-d70f-869fdd102804.png)

以下の設定を行います。

* Thing名: `dummy-temp-sensor`
* 説明: `ダミー温度センサー`

更新ボタンを押して設定を保存します。

以降で、Property、Action、Eventの実装を行います。

### Property

Propertyは、Thingの状態のことです。

Propertyの実装には、Server-Propertyノードを利用します。
Server-Propertyノードは、以下の機能を持ちます。

* Consumerからの読み込み要求を受信します
* Consumerからの書き込み要求を受信します
* Thingで発生したProperty値の変化をConsumerに通知します

今回は計測温度をConsumerに公開します。

キャンバスに配置した`Server-Property`ノードをダブルクリックし、プロパティ画面を表示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/9a2541fb-0671-a9bc-1a4b-818d788aa01f.png)

プロパティ画面に以下の情報を入力します。

* 名前: `Temperature`
* サーバ設定: `http-server`
* Thing設定: `dummy-temp-sensor`
* プロパティ名: `temperature`
* プロパティの説明: `計測した温度`
* データ型: `number`  
  → 計測温度は数値であるためnumberを指定
* 読み込みのみ: `✓`  
  → 計測温度は、読み込みのみのPropertyであるため
* 観測可能: `✓`
  → 計測温度の更新が発生した場合、Consumerに通知するため
* 書き込む値の出力先: `msg.payload`
  → 計測温度をConsumerから書き込むことがないため今回は利用しません(デフォルトのままとします)

Consumerに提供する機能は以下とします。

* 10秒ごとに温度を計測し、Consumerに通知します
* Consumerから読み込み要求を受信したら、最新の計測温度を返します  
  (今回はConcumerからの書き込み要求は扱いません)

フローは以下のように作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/46f3a964-fe82-0ceb-ef72-7bf59fc0457e.png)

各ノードの設定を示します。

#### Injectノード: タイムスタンプ

10秒毎に処理を実施するために、Injectノードを利用します。
Injectノードを配置し、ダブルクリックしてプロパティ画面を表示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/c5c32924-4209-c6db-7c44-91cbfd2a5dcd.png)

`繰り返し`に`指定した時間間隔`を設定し、`時間間隔`を10秒にします。

#### Functionノード: measure dummy temperature

InjectノードにFunctionノードを接続し、ダミーの温度計測を行います。
Functionノードで実行するプログラムは以下とします。

```javascript
let temp = flow.get("temp");
if(temp === undefined) {
    temp = 25;
} else {
    temp += Math.random() * 6 - 3;
}
flow.set("temp", temp);
msg.payload = temp;
return msg;
```

上記プログラムにより、ダミーの温度を、現状の温度+-3の範囲でランダムに生成します。
生成した温度は、フローコンテキストに保存し、別のノードからも参照できるようにします。
デフォルトの温度は25度とします。

ダミー温度は、msg.payloadに設定し、Server-Propertyノードに入力します。
Server-Propertyノードは、`観測可能`に設定しているため、ダミー温度が入力されるとその値をConsumerに送信します。

#### Functionノード: get temp value

Server-Propertyには、出力端子が2つあります。
1番目は、ConsumerからPropertyの読み込み要求があると出力される端子です。
2番目は、ConsumerからPropertyの書き込み要求があると出力される端子です。

今回は読み込み要求にのみ対応するため、1番目の端子にのみ後続のノードを接続します。

1番目の出力端子に接続したFunctionノードでは以下のプログラムを実行します。

```javascript
let temp = flow.get("temp");
if (temp === undefined) {
    temp = 25;
}
msg.payload = temp;
return msg;
```

上記プログラムにより、フローコンテキストに保存されたダミーの温度をmsg.payloadに設定します。

#### Server-Endノード

Consumerからの要求を終えるにはServer-Endノードを接続します。

Server-Endノードのプロパティ画面では、戻り値の入力元を設定できます。
今回はデフォルトのままmsg.payloadとします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/56ccbacb-2b5b-5902-f165-448c52b572d5.png)

msg.payloadには、ダミーの温度が設定されているためこの値がConsumerに返ります。

### Action

Actionは、ThingがConsumerに提供する機能のことです。

Actionの実装には、Server-Actionノードを利用します。
Server-Actionノードは、Consumerからの実行要求を受信すると出力を行います。

今回は、Consumerから送信された警告のためのしきい値温度を保存します。

Server-Actionノードをキャンバスに配置し、ダブルクリックしてプロパティ画面を表示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/a7ea90f6-58f3-4108-18ac-f303a3e52a4d.png)

プロパティ画面に以下の情報を入力します。

* 名前: `set temp threshold`
* サーバ設定: `http-server`
* Thing設定: `dummy-temp-sensor`
* アクション名: `setTempThreshold`
* アクションの説明: `警告のためのしきい値温度を設定`
* 引数のデータ型: `number`  
  → しきい値温度は数値であるためnumberを指定
* 戻り値のデータ型: `null`  
  → 戻り値はないためnullを指定
* 引数の出力先: `msg.payload`
  → 引数であるしきい値温度の格納先をmsg.payload(デフォルト)とします

フローは以下のように作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/10d0011a-6f48-ba19-ca28-dfd9efca1551.png)

各ノードの設定を示します。

#### Functionノード: store temp threshold

Server-Actionノードが出力したしきい値温度の引数をフローコンテキストに保存します。
Functionノードで実行するプログラムは以下です。

```javascript
const tempThreshold = msg.payload;
flow.set("tempThreshold", tempThreshold);
return msg;
```

#### Server-Endノード

Consumerからの要求を終えるために、Server-Endノードを接続します。
Server-Endノードのプロパティ画面はデフォルトのままとします。

### Event

Eventは、Thingで発生した事象をConsumerに通知する機能のことです。

Eventの実装には、Server-Eventノードを利用します。
Server-Eventノードにイベント情報の入力を行うと、Consumerへの通知を行います。

今回は、計測温度がしきい値温度を超えた場合にイベントを発生します。

Server-Eventノードをキャンバスに配置し、ダブルクリックしてプロパティ画面を表示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/8fab820c-b253-82cc-32c3-2e8c7ef5c315.png)

プロパティ画面に以下の情報を入力します。

* 名前: `temp alert`
* サーバ設定: `http-server`
* Thing設定: `dummy-temp-sensor`
* イベント名: `tempAlert`
* イベントの説明: `計測温度がしきい値以上になったため警告`
* データ型: `number`  
  → イベント値には計測温度(数値)を利用するためnumberを指定
* イベント値の入力元: `msg.payload`
  → イベント値である計測温度の入力元をmsg.payload(デフォルト)とします

フローは以下のように、Propertyのフローに`temperature threshold`ノードと`temp alert`ノードを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/e994e4f9-ee3e-e815-acc2-945492108861.png)

各ノードの設定を示します。

#### Functionノード: temperature threshold

`measure dummy temperature`ノードの出力である計測温度がしきい値以上かどうかを判定し、しきい値以上であった場合、出力端子に計測温度を出力します。
出力端子には、Server-Eventノードを接続しているためイベント通知が実行されます。

Functionノードで実行するプログラムは以下です。

```javascript
let temp = msg.payload;
let tempThreshold = flow.get("tempThreshold");
if(tempThreshold === undefined) {
    tempThreshold = 30;
}
if(temp > tempThreshold) {
    msg.payload = temp;
    return msg;
}
```

以上でThingフローの作成は終わりです。

## Thing Descriptionの参照

ConsumerとThingを連携させる際に利用するThing Descriptionの参照方法を示します。

Thing Descriptionは、作成したフローをデプロイした後、Node-REDエディタの右側にある`コンテキストデータ`タブで参照できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/d752ee41-6b7e-c604-5622-dde68577c5e3.png)

`グローバル`の右の更新ボタンを押下すると最新のThing Descriptionが表示されます。
Consumerの作成時に利用してください。

## Consumerフローの自動作成

`node-red-node-wot`プラグインには、Consumerのフローを自動的に作成する機能があります。
この機能を利用すると、Thingのインターフェースを操作する簡易的なダッシュボードを作成できます。

今回は、本記事で作成したダミー温度センサーのThingと連携するConsumerを作成しますが、Node-RED以外の環境で作成したThingと連携するConsumerであっても作成できます。
Concumerが動作するPCは、Thingが動作するPCとネットワーク接続された別のPCであっても問題ありません。別のPCを利用する場合は、Thingの[環境構築](#環境構築)の手順を実施して`node-red-node-wot`が動作する環境を構築してください。

自動作成するConsumerフローは、`@flowfuse/node-red-dashboard`ノードを利用しますので、Node-REDに追加します。

Node-REDエディタの右上のハンバーガーメニューを開き、`パレットの管理`をクリックします。

`ノードを追加`タブをクリックし、検索欄に`@flowfuse/node-red-dashboard`を入力すると該当するノードがいくつか見つかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/45a2b2b0-0c54-5ccc-7216-da618c75430b.png)

`@flowfuse/node-red-dashboard`の`ノードを追加`ボタンを押します。

`@flowfuse/node-red-dashboard をインストールします。`のダイアログが表示されたら`追加`ボタンを押します。

インストールが成功するとパレットに以下のノードが追加されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/62c482c6-07a7-05a3-946b-a3f09aaf8e5a.png)

次に、Consumerフローの自動生成に利用するThing Descriptionをコピーします。

Thingが動作するNode-REDで、[Thing Descriptionの参照](#thing-descriptionの参照)に従ってThing Descriptionを表示し、右のコピーボタンを押してJSON文字列をコピーしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/d78b649c-3a4d-7e2d-4031-af4fa9722e82.png)

Thing Descriptionをコピーしたら、右上のハンバーガーメニューをクリックし、`Create WoT Consumer flow`をクリックします。  

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/ec4103a4-eb14-8aa3-3fbc-29e895d87c1e.png)

もしハンバーガーメニューに`Create WoT Consumer flow`が表示されない場合は、ブラウザのリロードを実施してみてください。`Create WoT Consumer flow`を表示するには、Node-REDに`@thingweb/node-red-node-wot`を追加したあとブラウザのリロードが必要です。

`Create WoT Consumer flow`をクリックすると、Thing Descriptionを入力するためのダイアログが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/263ed5c9-9cc1-4902-2bd1-1624fce7d441.png)

表示されたダイアログに、コピーしたThing Descriptionを貼り付けます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/280d286b-2d6d-568b-d93a-7d8007f37975.png)

`OK`ボタンを押すと、新しいフロータブが開き、Consumerのフローが生成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/8de2307c-5939-c768-e71f-9d7e7201732e.png)

生成されたフローを適切な場所に貼り付け、右上のデプロイボタンを押してフローを保存してください。

Consumerフローが実現するダッシュボードを開くには、Node-REDエディタの右側にあるダッシュボード2.0タブを開き、`ダッシュボードを開く`ボタンを押してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/dd5d589f-17f1-5c88-6292-d4695daee7ab.png)

ブラウザで、以下のようなダッシュボードが開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/4c7235a6-9e2c-6a62-c001-1fc87d95d3da.png)

Property、Action、Eventの各部について説明します。

* Property: temperature  
Thingの計測温度を確認できます。
  * latest value: 最新の計測温度を表示します
  * temperature chart: 計測温度の時間経過をグラフ表示します(number、integer型のデータの場合はグラフが表示されます)
* Action: setTempThreshold  
温度のしきい値をThingに設定します(Thingで計測温度がしきい値を超えたか判定し超えた場合はtempAlertイベントを発生します)。
  * argument of action: 温度のしきい値を指定します
  * executeボタン: 温度のしきい値設定のActionを実行します
  * action invoked: 最後にActionを実行した時刻を表示します
  * result of action: Actionの戻り値を表示します(このActionではnullを返します)
* Event: tempAlert  
計測温度がしきい値を超えた場合にThingで発生したEventを表示します。
  * time: Eventの受信日時を表示します
  * source: Eventが発生したThingの名前を表示します
  * event: Eventの値を表示します(今回は計測温度を表示します)
  また、Eventが発生した場合は以下のようにダイアログも表示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99646/40d378bb-b26c-4bf1-9163-a905a282eabb.png)

以上でConsumerフローの作成は終わりです。

## おわりに

本記事では、`node-red-node-wot`ノードを利用して、Node-RED上でWoTのThingと、Consumerであるダッシュボードを作成する方法を説明しました。

今回作成したThingはダミーの温度センサーでしたが、Node-REDで利用できる公開ノードには、センサーなどのデバイスを操作するノードも多くあるため、`node-red-node-wot`と組み合わせて実際のデバイスと連携するThingを作成することも簡単です。

また、`node-red-node-wot`には、Consumerのフローを自動作成する機能があります。自動作成されるフローは簡易的なものですが、このフローを参考にすることで実用的なConsumerの開発が容易になると思います。

ぜひWoTを利用したIoTシステムの開発にチャレンジしてみてください！